Wiki

Canvas Item Groupies

by Warwick Allison

Diagram editors and similar programs often provide their users with a means of grouping graphical items together. Grouping makes it easier for users to apply an operation to many items at once. This article presents one simple approach to grouping canvas items using a generic CanvasGroup class. We also provide a QCanvasView subclass as a practical example of use.

Implementing the CanvasGroup Class

From an end-users point of view, many operations that can sensibly be applied to a single item should also be applicable to a group of items. For example, users should be able to scale, move, and delete items and groups in the same way. But from a developers point of view, membership of a group is really a property of an item, rather than an item itself, and this perspective informs the CanvasGroup class we present below.

#ifndef CANVASGROUP_H
#define CANVASGROUP_H

#include <qmap.h>
#include <qptrlist.h>

class QCanvasItem;
class QRect;

class CanvasGroup
{
public:
    typedef QPtrList<QCanvasItem> CList;
    typedef QPtrListIterator<QCanvasItem> CListIter;

CanvasGroup() {}
&nbsp;CanvasGroup();

void add( QCanvasItem* );
bool remove( QCanvasItem* );
void moveBy( double, double );
QRect boundingRect() const;
bool contains(const QCanvasItem* ) const;
int count() const { return items.count(); }

static CanvasGroup* groupContaining( QCanvasItem* );
static CanvasGroup* merge(QCanvasItem*,QCanvasItem*);
static CanvasGroup* merge( CList );

private:
    CList items;
    typedef QMap<QCanvasItem*,CanvasGroup*> GroupMap;
    static GroupMap& groupMap();
};
#endif

The CanvasGroup provides the functions that we would expect: add(), remove(), contains(), and count(). The class also maintains a global map of all the groups, allowing us to determine which group, if any, a canvas item belongs to. It also provides functions to merge groups and items together.

#include "canvasgroup.h"
#include <qcanvas.h>

CanvasGroup::&nbsp;CanvasGroup()
{
    for ( CListIter it(items); *it; ++it )
        groupMap().remove( *it );
}

When a canvas group is deleted, we remove its items from the global item-to-group map.

void CanvasGroup::add( QCanvasItem* item )
{
    CanvasGroup*& mapping = groupMap()[item];
    if ( mapping ) {
         if ( mapping == this )
             return;
         mapping->items.removeRef( item );
         if ( mapping->items.isEmpty() )
             delete mapping;
    }
    items.prepend( item );
    mapping = this;
}

When an item is added to a group we make sure that it is mapped to this group, and add it to this group's list of items.

bool CanvasGroup::remove( QCanvasItem* item )
{
    groupMap().remove( item );
    if ( items.removeRef(item ) ) {
        if ( items.isEmpty() )
            delete this;
        return true;
    } else
        return false;
}

To remove an item we must take it out of the global map and out of the list of items in this group.

void CanvasGroup::moveBy( double dx, double dy )
{
    for ( CListIter it(items); *it; ++it )
        (*it)->moveBy( dx, dy );
}

QRect CanvasGroup::boundingRect() const
{
    QRect rect;
    for ( CListIter it(items); *it; ++it )
        rect |= (*it)->boundingRect();
    return rect;
}

Calculating a bounding rectangle for the entire group is easy; we just OR the bounding rectangles of all the items in the group.

bool CanvasGroup::contains( const QCanvasItem* item )const
{
    for ( CListIter it(items); *it; ++it )
        if ( *it == item )
            return true;
    return false;
}

CanvasGroup* CanvasGroup::groupContaining(
                            QCanvasItem* item )
{
    GroupMap::ConstIterator it = groupMap().find( item );
    if ( it == groupMap().end() )
        return 0;
    else
        return *it;
}

CanvasGroup* CanvasGroup::merge( QCanvasItem* item1,
                                 QCanvasItem* item2 )
{
    CanvasGroup* group1 = groupContaining( item1 );
    CanvasGroup* group2 = groupContaining( item2 );
    if ( group1 ) {
        if ( group2 ) {
            if ( group1 == group2 )
                return group1;

          for ( CListIter it(group2->items); *it; ++it )
                group1->add( *it );
            delete group2;
        } else
            group1->add( item2 );
        return group1;
    } else if ( group2 ) {
        group2->add( item1 );
        return group2;
    } else {
        group1 = new CanvasGroup;
        group1->add( item1 );
        group1->add( item2 );
        return group1;
    }
}

Merging two groups has several cases. If one item is a group and the other isn't, we just add the ungrouped item to the grouped item's group. If they're both in a group, we add the second item's group's items to the first item's group. If neither item is in a group, we create a new group containing both items.

CanvasGroup* CanvasGroup::merge( CList list )
{
    if ( !list.isEmpty() ) {
        CListIter it(list);
        QCanvasItem* first = *it;
        ++it;
        while ( *it ) {
            merge( first, *it );
            ++it;
        }
    }
}

To merge a list of items into this group we recursively merge each succeeding item into the same group as the first item.

QMap<QCanvasItem*,CanvasGroup*>& CanvasGroup::groupMap()
{
    static GroupMap *group = 0;
    if ( !group )
        group = new GroupMap;
    return *group;
}

Using the CanvasGroup Class

The CanvasGroup implementation provides the facilities we need for handling groups of QCanvasItems, but it does not provide a visual representation of a group or any means of interaction. Both these outstanding issues can be solved through the use of a QCanvasView subclass such as the one presented below.

#ifndef CANVASVIEW_H
#define CANVASVIEW_H

#include "canvasgroup.h"
#include <qcanvas.h>

class CanvasGroup;
class QPoint;

class CanvasView : public QCanvasView
{
    Q_OBJECT
public:
    CanvasView( QCanvas *canvas, QWidget *parent = 0,
                const char *name = 0, WFlags f = 0 )
        : QCanvasView( canvas, parent, name, f ),
          selection( 0 ), selectionbox( 0 ) {}
    &nbsp;CanvasView() {}

void clear();
void ungroup();

void contentsMousePressEvent( QMouseEvent* evt );
void contentsMouseMoveEvent( QMouseEvent* evt );

private:
    CanvasGroup *selection;
    QCanvasRectangle *selectionbox;
    QPoint moving_start;
};
#endif

In this subclass we keep a pointer to the selected canvas group, and we use a QCanvasRectangle to provide a visual representation of the group on the canvas. We also need to hold a QPoint so that we can tell where the user began dragging when they move items or groups. To simplify the code, whenever the user selects a single item we create a CanvasGroup for that item. This allows us to treat all operations in terms of the current group which may contain one or more items.

#include "canvasview.h"
#include <qwmatrix.h>

void CanvasView::contentsMousePressEvent(
        QMouseEvent* evt )
{
    QPoint p = inverseWorldMatrix().map( evt->pos() );
    QCanvasItemList list = canvas()->collisions( p );

  if ( !( evt->state() & ShiftButton ) )
        selection = 0;

  for ( QCanvasItemList::Iterator it = list.begin();
          it != list.end(); ++it ) {
        if ( *it == selectionbox )
            continue;
        if ( !selection ) {
            selection = CanvasGroup::groupContaining(*it);
            if ( !selection ) {
                selection = new CanvasGroup;
                selection->add( *it );
            }
        } else if ( selection->contains( *it ) ) {
            if ( evt->state() & ShiftButton ) {
                moving_start = p;
                bool b = selection->count() == 1;
                selection->remove( *it );
                if ( b )
                    selection = 0;
                break;
            } else 
                return;
        } else 
            selection->add( *it );
        moving_start = p;
        break;
    }
    delete selectionbox;
    selectionbox = 0;
    if ( selection ) {
        selectionbox = new QCanvasRectangle(
                            selection->boundingRect(),
                            canvas() );
        selectionbox->setBrush( NoBrush );
        selectionbox->setPen( QPen(red, 0,
                                   DashDotDotLine) );
        selectionbox->setZ( 9999 );
        selectionbox->show();
    }
    canvas()->update();
}

Most of the action takes place in the contentsMousePressEvent(). If the user hasn't pressed the Shift key, we clear the current selection. We then iterate over the list of collisions for the point where the user clicked, ignoring the QCanvasRectangle that we use to represent the selection. If there is no current selection we add the items to a new selection; otherwise we add items to the current selection. Items are deselected if the user hasn't pressed Shift. Once the selection is up-to-date, we delete the rectangle used to visualize the selection and draw a new one if necessary.

void CanvasView::contentsMouseMoveEvent(QMouseEvent* evt)
{
    if ( selection ) {
        QPoint p = inverseWorldMatrix().map( evt->pos() );
        double dx = p.x() - moving_start.x();
        double dy = p.y() - moving_start.y();
        selection->moveBy( dx, dy );
        selectionbox->moveBy( dx, dy );
        moving_start = p;
        canvas()->update();
    }
}

void CanvasView::clear()
{
    ungroup();
    QCanvasItemList list = canvas()->allItems();
    QCanvasItemList::Iterator it = list.begin();
    for ( ; it != list.end(); ++it ) {
        if ( *it )
            delete *it;
    }
    canvas()->update();
}

void CanvasView::ungroup()
{
    delete selection;
    delete selectionbox;
    selection = 0;
    selectionbox = 0;
    canvas()->update();
}

If the user drags a selection we use CanvasGroup's moveBy() function to move the selection's items. Clearing all the items also requires that we deselect any selected items.

Summing Up

Canvas

We have presented a CanvasGroup class and a CanvasView subclass that are sufficient to provide basic grouping services. The screenshot shows a very simple application that allows a user to add colored circles to a canvas. The user can Shift+Click these circles to group them, and can drag groups (items are groups of one) around the canvas.

The CanvasGroup and CanvasView classes, along with the small test application shown above, are available in this ZIP file (12K).


Copyright © 2003 Trolltech. Trademarks