Detecting clicks on a QGraphicsItemGroup

I've been putting together an image-slider to animate images based on some pushbuttons. As an example for the effect I was looking for see the top of this page (http://www.menucool.com/slider/javascript-image-slider-demo4).

I've succeeded in this part of the process by subclassing a QObject/QGraphicsItemGroup (coded in imageSlider.h and imageSlider.cpp) and adding animation to it. The code may be of use to others attempting similar.

Following this I wanted to add the ability to click on the imageSlider, which would emit a signal clicked(), whereby the selected image could be queried and some task carried out based on that selection. Sounded easy.

Attempt 1:

My first, possibly most obvious attempt, was to reimplement a mouseReleaseEvent handler in imageSlider to catch the mouse clicks. However, this function never gets called.

Attempt 2:

The imageSlider is added to a QGraphicsScene which is then viewed through a QGraphicsView. It made sense that perhaps the QGraphicsView was handling the mouse clicks and they were therefore not getting to the imageSlider at all. So I subclassed QGraphicsView to make a class called clickableQGraphicsView which has a reimplemented mouseReleaseEvent. This does work. While I can use this method and move on, for elegance sake I would like to encapsulate all the code required to interact with the imageSlider into the imageSlider code itself. If I were to use this method it requires two classes.

Attempt 5: (attempts 3 & 4 didn't work out)

I thought it might be possible to pass the events detected by the regular QGraphicsView down to an event handler in imageSlider. This would mean I could use the standard QGraphicsView class and have the ability in the imageSlider to detect its own mouse clicks. I installed the event filter. This event handler in imageSlider gets called on any move of the mouse within the QGraphicsView, so the filter appears to work. Strangely however, mouse clicks events are not detected.

So, the question:

I'm sure you're wondering why I don't just shut up and use attempt 2. I'm curious however, and I'd be interested for my own education as to the reasons attempts 1 and 5 don't work.

The easiest way to test this would be to download the project code from: https://www.dropbox.com/sh/zxlo3n014v7g2n7/riMq3FCB4i The pro file is in the TestGUI folder.

The various attempts can be compiled by changing the #define at the top of imageSlider.h. If all the defines are commented out, then no click detection is enabled. Each of the attempts can be tested by uncommenting the appropriate #define line.

If preferred, the affected code is pasted below.


The imageSlider.h code is also shown below:

    class imageSlider: public QObject, public QGraphicsItemGroup
{
    Q_OBJECT
    Q_PROPERTY(QPointF pos READ pos WRITE setPos)   /// these property access functions don't have to be reimplemented as they are implemented in QObject. Just declare them.

public:
    imageSlider(QGraphicsItem *parent = 0);
    ~imageSlider();

    virtual void addToGroup(QGraphicsItem *item);   // /virtual function is used so that the function can be reimplemented but using the same signature (name, arguments, etc)
    virtual void removeFromGroup(QGraphicsItem *item);
    void moveImage(int numImages);
    void setTransitionDuration(uint ms);
    void setEasingCurve(uint curveNum); //see the help file for QEasingCurve to see what the integers stand for

private:
    QList <QGraphicsItem*> items;   /// holds the images themselves
    uint transitionDuration; /// The duration of the transition in ms
    int currentImageIndex; /// The index of the currently displayed image
    QMutex animationMutexLock;  /// So that an animation cannot be called while the previous one is ongoing
    QEasingCurve::Type easingCurveType; /// the type of easing curve for the animation.

    int getXPosChange(int numImages);   /// Get the amount the position has to change by to slide by numImages images
    void animate(int xPosChange);   /// Move the images along by the number of pixels in xPosChange

public slots:
    void unlockMutex();

signals:
    void clicked(); /// Parent process can be connected to this. When clicked the getCurrentImageIndex() function can be called and a response carried out (like running a process, opening a link, etc).

    //*******************
    // TEST CODE
    //*******************
#ifdef MOUSECLICKDETECTATTEMPT1
public slots:
    void mouseReleaseEvent(QGraphicsSceneMouseEvent * event);
#endif
#ifdef MOUSECLICKDETECTATTEMPT5
protected:
    bool eventFilter(QObject *target, QEvent *event);
#endif

};

And the imageSlider.cpp is:

#include "imageSlider.h"

/**
  * Constructor for image slider.
  */
imageSlider::imageSlider(QGraphicsItem *parent) :
    QObject(0),
    QGraphicsItemGroup(parent),
    transitionDuration(500),
    currentImageIndex(0),
    easingCurveType(QEasingCurve::Linear)
{
}

/**
  * Deconstructor for image slider.
  */
imageSlider::~imageSlider()
{
    if(~items.isEmpty())
        items.clear();
}

/**
  * Add QGraphicsItems (images from QPixmaps) to the image slider.
  */
void imageSlider::addToGroup(QGraphicsItem *item){
    //change the xpos of the item before adding so that the images line up one after the other
    int xpos = 0;
    for(int i=0;i<items.count();i++)
        xpos += items.at(i)->boundingRect().width();
    item->setX(xpos);
    //add to the items and group
    items << item;
    QGraphicsItemGroup::addToGroup(item);
}

/**
  * Remove items from the imageSlider.
  */
void imageSlider::removeFromGroup(QGraphicsItem *item)
{
    items.removeAll(item);
    QGraphicsItemGroup::removeFromGroup(item);
}

/**
  * Move the imageSlider along by numImages number of images.
  * numImages can be +ve (move images left) or -ve (move images right).
  */
void imageSlider::moveImage(int numImages)
{
    if(animationMutexLock.tryLock())    //the mutex will be unlocked on receiving the finished() signal from the animation object
    {
        int xPosChange = getXPosChange(numImages);
        if(xPosChange==0)   //not going to move, so unlock the mutex here as otherwise you have to wait for a zero move animation to finish before getting the next animation. Not a bug, but not ideal for user fluidity.
            animationMutexLock.unlock();
        else
            //Do the animation
            imageSlider::animate(xPosChange);
    }
}

/**
  * Calculate the distance the QGraphicsItemGroup must slide to show the rqeuired image.
  * A positive numImages move moves the current image to the left.
  * A negative numImages move moves the current image to the right.
  */
int imageSlider::getXPosChange(int numImages)
{
    //create an incrementer that increments up or down to the requested image
    int incrementer = 1;
    if(numImages<0)
        incrementer = -1;
    int imageToGoTo = currentImageIndex + numImages;
    //check that the requested image is within the bounds of the number of images we have
    if(imageToGoTo<0)
        imageToGoTo = 0;
    if(imageToGoTo>items.count()-1)
        imageToGoTo = items.count()-1;
    //calculate the positional change
    int posChange = 0;
    int i=0;
    for(i=currentImageIndex;i!=imageToGoTo;i+=incrementer)
        posChange += items.at(i)->boundingRect().width();   //add the width of each image to skip over
    //update the current image index to the image that will be shown
    currentImageIndex = imageToGoTo;
    //if we want to move to the right, make the positional change negative
    if(incrementer==1)
        posChange = -posChange;
    return posChange;
}

/**
  * Carry out the animation from one image to another.
  */
void imageSlider::animate(int xPosChange)
{
    QPointF currPos = this->pos();
    QPointF endPos = currPos;
    endPos.setX(currPos.x()+xPosChange);

    QPropertyAnimation *animation = new QPropertyAnimation(this,"pos");
    connect(animation,SIGNAL(finished()),SLOT(unlockMutex()));
    animation->setStartValue(currPos);
    animation->setEndValue(endPos);
    animation->setDuration(this->transitionDuration);
    animation->setEasingCurve(this->easingCurveType);
    animation->start(QAbstractAnimation::DeleteWhenStopped);
}

/**
  * A slot which is called when the animation is completed to unlock the mutex.
  * This mutex stops two animations from occurring at the same time.
  */
void imageSlider::unlockMutex()
{
    this->animationMutexLock.unlock();
}

/**
  * set function for the animation transition duration.
  */
void imageSlider::setTransitionDuration(uint ms)
{
    this->transitionDuration = ms;
}

/**
  * set functionfor the easing curve enum.
  */
void imageSlider::setEasingCurve(uint curveNum)
{
    this->easingCurveType = (QEasingCurve::Type)curveNum;
}

//*******************
// TEST CODE
//*******************
#ifdef MOUSECLICKDETECTATTEMPT1
/**
  * Try reimplementing the mouseReleaseEvent for the imageSlider to catch mouse clicks.
  */
void imageSlider::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
{
    qDebug() << "imageSlider mouse release event detected";
    emit clicked();
}
#endif

#ifdef MOUSECLICKDETECTATTEMPT5
/**
  * Try capturing clicks on the images within the slider using a QObject event filter (imageSlider inherits from QObject and QGraphicsItemGroup.
  */
bool imageSlider::eventFilter(QObject *target, QEvent *event)
{
    if(event->type()==QEvent::MouseButtonRelease)
    {
        qDebug() << "imageSlider mouse release event detected through the eventFilter";
        emit clicked();
        return true;
    }
    return false;
}
#endif

The main form code mainWindow.h is:

// 
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QGraphicsScene>
#include <QGraphicsItem>
#include <QPropertyAnimation>
#include "../imageSlider/imageSlider.h"
#include <QGraphicsView>

namespace Ui {
class MainWindow;
}

#ifdef MOUSECLICKDETECTATTEMPT2
class clickableQGraphicsView : public QGraphicsView
{
    Q_OBJECT

public:
    clickableQGraphicsView(QWidget *parent=0);
    ~clickableQGraphicsView();
public slots:
    virtual void mouseReleaseEvent(QMouseEvent *event);
};
#endif

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();

private slots:
    void on_pushButtonRight_clicked();
    void on_pushButtonLeft_clicked();
    void on_listWidgetAnimType_currentTextChanged(const QString &currentText);
    void on_spinBoxMSAnim_valueChanged(int arg1);
    void imageGroupClicked();

private:
    Ui::MainWindow *ui;
    imageSlider *imageGroup;
    QGraphicsScene *GScene;
    void moveImageSlider(int numImages);

    //****************
    // TEST CODE
    //****************
#ifdef MOUSECLICKDETECTATTEMPT2
    clickableQGraphicsView *clickView;
#endif

};

#endif // MAINWINDOW_H

And finally, mainWindow.cpp is:

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "qmessagebox.h"

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    //*******************
    // Set up options list
    // for animation types
    //*******************
    for(int i=0;i<41;i++)
        ui->listWidgetAnimType->addItem(QString::number(i));

    //****************
    // Graphics Scene
    //****************
    QPixmap pixmap1(":imageSwipe/images/1.png");
    QGraphicsItem *Image1 = new QGraphicsPixmapItem(pixmap1);
    QPixmap pixmap2(":imageSwipe/images/2.png");
    QGraphicsItem *Image2 = new QGraphicsPixmapItem(pixmap2);
    QPixmap pixmap3(":imageSwipe/images/3.png");
    QGraphicsItem *Image3 = new QGraphicsPixmapItem(pixmap3);
    QPixmap pixmap4(":imageSwipe/images/4.png");
    QGraphicsItem *Image4 = new QGraphicsPixmapItem(pixmap4);

    GScene = new QGraphicsScene();
    GScene->setSceneRect(QRectF(0,0,Image1->boundingRect().width(),Image1->boundingRect().height()));

    imageGroup = new imageSlider;
    imageGroup->addToGroup(Image1);
    imageGroup->addToGroup(Image2);
    imageGroup->addToGroup(Image3);
    imageGroup->addToGroup(Image4);

    GScene->addItem(imageGroup);

    ui->graphicsViewGUIInterface->setScene(GScene);
    ui->graphicsViewGUIInterface->setGeometry(0,0,Image1->boundingRect().width(),Image1->boundingRect().height());
    //*******************
    // TEST CODE
    //*******************
    connect(imageGroup,SIGNAL(clicked()),this,SLOT(imageGroupClicked()));

#ifdef MOUSECLICKDETECTATTEMPT2
    clickView = new clickableQGraphicsView(this);
    clickView->setScene(GScene);
    clickView->setGeometry(20,20,Image1->boundingRect().width(),Image1->boundingRect().height());
#endif

#ifdef MOUSECLICKDETECTATTEMPT5
    ui->graphicsViewGUIInterface->installEventFilter(imageGroup);
#endif
}

MainWindow::~MainWindow()
{
    if(imageGroup)
    {
        disconnect(imageGroup);
        delete imageGroup;
    }
    if(GScene)
        delete GScene;
    delete ui;
}

void MainWindow::on_pushButtonRight_clicked()
{
    moveImageSlider(-1);
}

void MainWindow::on_pushButtonLeft_clicked()
{
    moveImageSlider(1);
}

void MainWindow::moveImageSlider(int numImages)
{
    imageGroup->moveImage(numImages);
}

void MainWindow::on_listWidgetAnimType_currentTextChanged(const QString &currentText)
{
    imageGroup->setEasingCurve(currentText.toUInt());
}

void MainWindow::on_spinBoxMSAnim_valueChanged(int arg1)
{
    imageGroup->setTransitionDuration((uint)arg1);
}

void MainWindow::imageGroupClicked()
{
    QMessageBox msgBox;
    msgBox.setText(QString("Received index = 1"));
    msgBox.exec();
}

//***************
// TEST CODE
//***************
#ifdef MOUSECLICKDETECTATTEMPT2
/**
  * The below functions are an attempt to subclass the QGraphicsView
  * to provide a click response.
  */
clickableQGraphicsView::clickableQGraphicsView(QWidget *parent) :
    QGraphicsView(parent)
{
}

clickableQGraphicsView::~clickableQGraphicsView()
{
}

void clickableQGraphicsView::mouseReleaseEvent(QMouseEvent *event)
{
    if(event->type() == QEvent::MouseButtonRelease)
        qDebug() << "QGraphicsView event dectected";

}
#endif

Thanks for any help. I hope this may be of use to others too. Stephen

Answers


Ok I'm not very experienced with objects mentioned in question to be precise, but maybe just reading manual thoroughly and a little bit of debugging would be enough. Let's check it out:

1) First thing I would like to note that using binary not on bool values is a bit counter-intuitive and not a good practice, my VS even gave me warning about it. I mean this line to be exact: if(~items.isEmpty()).

2) When you get into Qt manual on QGraphicsItem::mouseReleaseEvent you'll find these lines:

Please note that mousePressEvent() decides which graphics item it is that receives mouse events. See the mousePressEvent() description for details.

So we're going to QGraphicsItem::mousePressEvent what we find is:

The mouse press event decides which item should become the mouse grabber (see QGraphicsScene::mouseGrabberItem()). If you do not reimplement this function, the press event will propagate to any topmost item beneath this item, and no other mouse events will be delivered to this item.

So basically to resolve problem with your first attempt we just gotta override mousePressEvent with function doing nothing {} and events will come into mouseReleaseEvent of imageSlider

3) About method 5, that was a bit tricky to figure out. Basically that's not working because of special nature of QGraphicsView -- it always translates event of type QEvent::MouseButtonRelease to it's special QEvent::GraphicsSceneMouseRelease type and sends it to it's mouse grabbers (mentioned previously), but even if there isn't any of those mouse grabbers it still sets GraphicsSceneMouseRelease event as accepted and thus MouseButtonRelease also becomes accepted, so in the end it's never being sent to further eventFilters. By the way MousePressEvent isn't being eaten like this for example.

Actually using setEventFilter is kinda too general way to do things most of the time, so anyway I guess you should stick to the way mentioned first.


Need Your Help

How to enable Emmet's tab key handler (HTML shortcuts) within JavaScript files?

javascript sublimetext3 sublimetext react-jsx emmet

By default the tab key handler only works within JavaScript strings. I'd like to use it within JSX files which have a .JS file extension (and the tags do not occur within strings.)

Dividing polygon value according to number of raster cells overlayed in R

r polygon raster

At the moment I'm working with the raster package. I've different polygons with certain values (let's say 100), which I managed to rasterize. The problem is that when I rasterize each raster cell r...