I would like to have a drag-and-drop feature based on images. If I drag and drop an image I would like to know which image I picked out and moved with (a simple std::string will do to uniquely identify the object that that image is representing). My thought is to store my own object (QPixmapItem) in the QGraphicsScene:
#ifndef QPIXMAPITEM_H
#define QPIXMAPITEM_H
#include <QGraphicsPixmapItem>
#include <QPoint>
class QPixmapItem : public QGraphicsPixmapItem
{
public:
QPixmapItem(std::string path, std::string id, int x, int y);
std::string getIdentifier (){return this->identifier;}
QPoint getPosition () const{return this->position;}
private:
std::string identifier;
QPoint position;
};
#endif // QPIXMAPITEM_H
This is the method I use to add my objects to the scene:
void MainWindow::addPixmapItemToScene(std::string path, int x, int y)
{
// generate pixmap item & add it to the scene
QPixmapItem *item = new QPixmapItem(path, std::string("id123"), x, y);
ui->roomView->scene()->addItem(item);
// only update the affected area
ui->roomView->updateSceneRect(item->pixmap().rect());
}
Here is how I try to 'catch' the object at a QMouseEvent:
void MainWindow::mousePressEvent(QMouseEvent *event)
{
std::cout << "mouse pressed" << std::endl;
QGraphicsPixmapItem *currentItem = dynamic_cast<QGraphicsPixmapItem *>(childAt(event->pos()));
if (!currentItem) {
return;
}
std::cout << "item pressed" << std::endl;
}
The objects are being added to the scene but whenever I press them, the final line ("item pressed") never makes it onto the screen..
QGraphicsItem already support moving by way of drag-and-drop within a QGraphicsScene. All you need is set the QGraphicsItem::ItemIsMovable flag.
If you want to get notified when that happens, override QGraphicsItem::itemChange() in your custom QGraphicsItem.
childAt() won't work because it returns a QWidget, and QGraphicsPixMapItems are not QWidgets (therefore childAt() will never return a pointer to any kind of QGraphicsItem, and even if it somehow did, the dynamic_cast conversion would return NULL anyway).
In order to get a list of QGraphicsItems that intersect a specified point in the scene, call QGraphicsScene::items() on your scene object. Then iterate through the returned list to find out what QGraphicsPixMapItems (if any) are in it.
Related
I am creating a simple gauge in Qt 4.7.4, and everything is working wonderfully. Except for the fact that, for the life of me, I cannot get the dial shape to paint over the text labels when it passes over them. It always paints it behind the label. I am just using a simple drawpolygon() method.
I'm thinking this has something to do about paint events? I am drawing everything inside a QFrame inside a MainWindow. I am using QFrame's paintEvent.
Edit:
The QLabels are created on start up with new QLabel(this). They are only created once, and never touched again ( Similar to manually adding them on the Ui with Designer). The drawpolygon() is in the QFrame's Paint event.
"myclass.h"
class gauge : public QFrame
{
Q_OBJECT
public:
explicit gauge(QWidget *parent = 0);
~gauge();
void setValues(int req, int Limit, bool extra=false);
private:
void drawDial();
protected:
void paintEvent(QPaintEvent *e);
};
"myclass.cpp"
void gauge::paintEvent(QPaintEvent *e)
{
Q_UNUSED(e);
drawDial();
return;
}
void gauge::drawDial()
{
QPainter Needle(this);
Needle.save();
Needle.setRenderHint(Needle.Antialiasing, true); // Needle was Staggered looking, This will make it smooth
Needle.translate(centrePt); // Center of Widget
Needle.drawEllipse(QPoint(0,0),10,10);
Needle.restore();
Needle.end();
}
If the gauge widget and the QLabels are siblings, then you can move the gauge widget to the front by calling its raise() method.
If the QLabels are children of the gauge widget, on the other hand, then they will always display in front of it. In that case you can either reorganize your widget hierarchy so that they are siblings instead, or you can get rid of the QLabels and simply call drawText() from your paintEvent() method instead (after drawDial() returns)
And again, trying to implement bezier curves redactor. There is
class BezierNode : public QGraphicsItem
BezierNode::BezierNode(QPointF point, Type type) : QGraphicsItem()
{
setPos(point);
setFlags(ItemIsMovable | ItemSendsScenePositionChanges | ItemSendsGeometryChanges);
}
It properly moves around in scene on mousePress + mouseMove, and I can catch events in itemChange() for some additional acting with control points. In QGraphicsItem::mouseMoveEvent() (according to Qt source on gitorious) there is a call to item->setPos(...). However, if I try to reimplement BezierNode::setPos(..), it's never triggered on moving object.
void BezierNode::setPos(const QPointF &pos) {
qDebug() << "setPos " << pos;
m_point = pos;
QGraphicsItem::setPos(pos);
}
In my case setPos() triggers only in constructor (there I call it manually). Yes, it moves in scene properly, I can get its position with pos() and use it everywhere instead of m_point, but I want to understand, what happens there.
Thanks in advance.
QGraphicsItem::setPos() is not virtual, so you can't override it. That's why BezierNode::setPos() will never be called.
The code below displays thumbnails in a left pane. When a thumbnail is clicked, the full-size image appears in the right pane.
I have the impression that even though this code is rather brief, it is not the most natural way to do this task in Qt. Am I reinventing the wheel? Are there Model-View classes that are more suitable for this task?
// main.cpp
#include "PixmapPair.h"
#include <QLabel>
#include <QWidget>
#include <QApplication>
#include <QSplitter>
#include <QGridLayout>
int main(int argc, char* argv[])
{
QApplication app(argc, argv);
QSplitter* page = new QSplitter;
QGridLayout* gridLayout = new QGridLayout;
QWidget* leftPane = new QWidget(page);
leftPane->setLayout(gridLayout);
QLabel* rightPane = new QLabel(page);
PixmapPair pair1(":/images/ocean.jpg", gridLayout, rightPane);
PixmapPair pair2(":/images/forest.jpg", gridLayout, rightPane);
page->setWindowTitle("Images");
page->show();
return app.exec();
}
// PixmapPair.h
#include <QPixmap>
#include <QIcon>
#include <QLabel>
#include <QPushButton>
#include <QGridLayout>
class PixmapPair : public QObject
{
Q_OBJECT
public:
PixmapPair(QString file, QGridLayout * gridLayout, QLabel* rp)
: rightPane(rp), largePixmap(file)
{
smallPixmap = largePixmap.scaled(QSize(100,100), Qt::KeepAspectRatio, Qt::SmoothTransformation);
QPushButton* pushButton = new QPushButton;
pushButton->setIcon(QIcon(smallPixmap));
pushButton->setFlat(true);
pushButton->setIconSize(QSize(100,100));
QObject::connect(pushButton, SIGNAL(clicked()), SLOT(displayInRightPane()));
gridLayout->addWidget(pushButton);
}
public slots:
void displayInRightPane()
{
rightPane->setPixmap(largePixmap);
}
private:
QLabel* rightPane;
QPixmap largePixmap;
QPixmap smallPixmap;
};
The left part of the SplitView is basically a list presenting all the available pictures. Qt provides a way to handle this using the model/view pattern.
The class for showing a list is a QListView, it will do the job automatically based on a model given with the function setModel().
This function requires a QAbstractItemModel, since this class is a pure abstract one we will need to create a custom class deriving from it.
Inheriting from it will require a lot of glue code but thankfully Qt already provides a class that takes care of most of it when we want to represent a list, it is called QAbstractListModel.
So I created an ImageListModel like this :
///////////////////////
// imagelistmodel.h ///
#ifndef IMAGELISTMODEL_H
#define IMAGELISTMODEL_H
#include <QAbstractListModel>
#include <QPixmap>
struct PixmapPair
{
QString _file;
QPixmap _small;
QPixmap _large;
};
class ImageListModel : public QAbstractListModel
{
Q_OBJECT
public:
// QAbstractItemModel retrieves various information (like text, color, ...)
// from the same index using roles. We can define custom ones, however to
// avoid a clash with predefined roles, ours must start at Qt::UserRole.
// All numbers below this one are reserved for Qt internals.
enum Roles
{
LargePixmapRole = Qt::UserRole + 1
};
explicit ImageListModel(std::initializer_list<QString> files, QObject *parent = 0);
virtual ~ImageListModel();
// QAbstractItemModel interface ===========================
public:
int rowCount(const QModelIndex &parent) const;
QVariant data(const QModelIndex &index, int role) const;
// ========================================================
private:
QList<PixmapPair*> _data;
};
#endif // IMAGELISTMODEL_H
///////////////////////
// imagelistmodel.cpp /
#include "imagelistmodel.h"
ImageListModel::ImageListModel(std::initializer_list<QString> files, QObject *parent)
: QAbstractListModel(parent)
{
auto iter = files.begin();
while (iter != files.end())
{
QPixmap large(*iter);
PixmapPair *pair = new PixmapPair();
pair->_file = *iter;
pair->_large = large;
pair->_small = large.scaled(100, 100, Qt::KeepAspectRatio, Qt::SmoothTransformation);
_data.append(pair);
++iter;
}
}
ImageListModel::~ImageListModel()
{
qDeleteAll(_data);
}
int ImageListModel::rowCount(const QModelIndex &parent) const
{
// This function should return the number of rows contained in the parent
// parameter, the parent parameter is used for trees in order to retrieve the
// number of rows contained in each node. Since we are doing a list each element
// doesn't have child nodes so we return 0
// By convention an invalid parent means the topmost level of a tree. In our case
// we return the number of elements contained in our data store.
if (parent.isValid())
return 0;
else
return _data.count();
}
QVariant ImageListModel::data(const QModelIndex &index, int role) const
{
if (index.isValid())
{
switch (role)
{
case Qt::DecorationRole:
{
// DecorationRole = Icon show for a list
return _data.value(index.row())->_small;
}
case Qt::DisplayRole:
{
// DisplayRole = Displayed text
return _data.value(index.row())->_file;
}
case LargePixmapRole:
{
// This is a custom role, it will help us getting the pixmap more
// easily later.
return _data.value(index.row())->_large;
}
}
}
// returning a default constructed QVariant, will let Views knows we have nothing
// to do and we let the default behavior of the view do work for us.
return QVariant();
}
///////////////////////
Our list is now ready and we are almost done.
// main.cpp ///////////
#include <QApplication>
#include <QSplitter>
#include <QLabel>
#include <QListView>
#include "imagelistmodel.h"
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QSplitter page;
QListView *imageList = new QListView(&page);
imageList->setModel(new ImageListModel({ "ocean.jpg", "forest.jpg" }, imageList));
// We tell the list view to show our icon, this mode will call the data function
// of our model with the role : DecorationRole.
imageList->setViewMode(QListView::IconMode);
// We want our list to show data vertically
imageList->setFlow(QListView::TopToBottom);
// We allow only one selection at a time in the list
imageList->setSelectionMode(QListView::SingleSelection);
QLabel *imagePresenter = new QLabel(&page);
// We connect to the signal emitted when the selection is changed
// to update the image presenter.
QObject::connect(imageList->selectionModel(), &QItemSelectionModel::selectionChanged, [imageList, imagePresenter] {
QModelIndex selectedIndex = imageList->selectionModel()->selectedIndexes().first();
// We use our custom role here to retrieve the large image using the selected
// index.
imagePresenter->setPixmap(selectedIndex.data(ImageListModel::LargePixmapRole).value<QPixmap>());
});
page.setWindowTitle("Images");
page.show();
return a.exec();
}
Advantages for this solution are:
- We can easily add filtering by wrapping our custom ListModel into a QSortFilterProxyModel.
- No need to create and manage a lot of buttons.
- The model never needs to know who shows it on screen.
- The QListView will autoscroll if necessary.
- Using a custom role allows us to easily retrieve the large image. If we added the large image in another column, it would show when using this model with a QTableView and when we want retrieve it from the selected index we would have to create a new index pointing to the right column. (Not really hard but require a little more code, and prone to error if we wrap the model in a ProxyModel)
Lambda explanation
For the lambda in C++ the full syntax is:
[CAPTURES]\(PARAMETERS\)->RESULT {FUNCTION}.
Between brackets we capture variables to be able to use them inside the FUNCTION without having to pass them as parameters.
The PARAMETERS between parenthesis have the same signification as any other function, if omitted the lambda takes no parameters.
RESULT is the return type of the FUNCTION and can be omitted.
FUNCTION the body to execute
In this example I decided to ignore the parameters given by the signal so I omitted the parenthesis. I use the captured controls to retrieve the user selection and update the picture shown.
I have some custom QGraphicsItems in a QGraphicsView of a QGraphicsScene.
I wish to know if there is an Item in the view at given (x,y) coordinates.
To test purpose I use the following class as QGraphicsScene:
class CustomScene : public QGraphicsScene
{
protected:
void mousePressEvent(QGraphicsSceneMouseEvent *event)
{
if (QGraphicsItem *item = itemAt(event->scenePos())) {
qDebug() << "You clicked on item" << item;
}
else {
qDebug() << "You didn't click on an item.";
}
}
};
In my application I have:
a class "Screen::Screen(QWidget *parent): QWidget(parent){...}" with inside an instance of my class "CustomScene : public QGraphicsScene {...}" described above and an instance of class QGraphicsView.
some instances of my class "Rect : public QGraphicsRectItem {...}", added to the QGraphicsView,s that draw some rectangles after some calculations.
When I launch the application and click on the drawn rectangles, I always have "You didn't click on an item." message.
Searching here in previous posts or searching on google I didn't find the reason why my code doesn't work. What am I doing wrong?
Edit 1: boundingRect() method returns correctly. I tried to add some QGraphicsRectItem, itemAt() method returns their information correctly.
The problem was that I didn't override the QPainterPath QGraphicsItem::shape () const [virtual] method.
Once done, itemAt() method started to works as expected.
I have 2 custom qgraphicsitems on a qgraphicsScene, rendered by a qgraphicsview. Now I want to be able to drag and drop one of the 2 items to the other kind. But which events should I reimplement for this? The documentation is a bit confusing on this.
also I want the qgraphicsitem to jump back to its original position if the user drags it to another area than the qgraphicsitem it should be dropped on.
As far as i know this is not implemented in the QGraphicsScene itself.
You must derive your own class from QGraphicsView or QGraphicsScene and then overload:
class MyGraphicsView : public QGraphicsView
{
Q_OBJECT;
protected:
virtual void mousePressEvent(QMouseEvent* event);
virtual void mouseMoveEvent(QMouseEvent* event);
virtual void mouseReleaseEvent(QMouseEvent* event);
...
private:
QGraphicsItem *currentDraggedItem;
};
QGraphicsView gives works with view/window coordinates while QGraphicsScene works with Scene coordinates.
Add code like:
void MyGraphicsView::mousePressEvent(QMouseEvent* event)
{
currentDraggedItem = itemAt(event->pos());
QGraphicsView::mousePressEvent(event);
}
void MyGraphicsView::mouseReleaseEvent(QMouseEvent* event)
{
QGraphicsItem *foundItem = itemAt(event->pos());
if(foundItem && currentDraggedItem &&
foundItem != currentDraggedItem)
{
// Handle DragDrop Here
}
QGraphicsView::mouseReleaseEvent(event);
}
This does the job for one QGaphicsScene. If you have two of them - the both have to know each other and you must translate coordinates from the one QGraphicsView to the other QGraphicsView. using mapTo...().
The key to this is checking the QGraphicsItems rect and seeing if they intersect.
So, when the mouse down is pressed on an item, store its current position. You can now move it and wait for the mouse release. On the release of the mouse button, check if the bounding rects of the items intersect with QRect::contains(const QRectF). If they do, then you've dropped one onto the other. If not, then animate the graphics item back to the previously stored position.
Just make sure that when you're checking the bounding rects for intersection that you're doing this with both of them in scene space coordinates. Either convert them, or use QGraphicsItem::sceneBoundingRect().