Reputation: 150
Hello ! Qt gives us the mean to create highly customized graphics items. All we need to do is to inherit from QGraphicsItem and override the pure virtual boundingRect() function. Moreover, we can optionally override the virtual shape() function to (among others) give items a more accurate shape...
Now let's see the graph below I drew with a software (personal student project) I've been developing with Qt in C++.
Then let's gray-highlight the bounding rectangle of each edge within the above-pictured graph.
I want the items to be selectable so I enable the selection flag:
setFlag(ItemIsSelectable, true);
It works like a dream for rectangular and circle items. It works for edges too, but not like a charm. Indeed edges are still selected if I click in the area defined by the the bounding rectangle (the gray one in the previous graph). Is there a way to ensure that the mouse-clicked event is taken into account only if we clicked on the curve defining the item shape?
I've overridden all the mouse*Event and return if shape() doesn't intersect with event.scenePos() but the outcome wasn't better. Is there a way to achieve what I want to do? Is there a Qt-ish way to check wether the mouse position is within the curve path?
Actually I finally end up setting a flag so that edges ignore mouse buttons:
setAcceptedMouseButtons(Qt::NoButton);
But I would be happy if somebody faced a similar issue and has a solution to share.
Here is a portion of code that you can compile and execute. As a reminder, I want edges (curves) to be selected only if we click on the path defining its shape.
/**
* It was really hard to come up with the little snippet below,
* since the real code is more complex.
* Hope someone'll be able to provide me with a solution.
*
* All you need to do is to copy and paste the code to a main.cpp file.
*/
#include <QApplication>
#include <QGraphicsItem>
#include <QGraphicsRectItem>
#include <QGraphicsView>
#include <QScrollBar>
/**
* Nothing special about this class.
* Note View instances handle rubber band selection (you can try it).
*/
class View : public QGraphicsView {
public:
View(QWidget *parent = nullptr)
: QGraphicsView(parent)
{
customize();
}
private:
void customize() // just customization
{
horizontalScrollBar()->setContextMenuPolicy(Qt::NoContextMenu);
verticalScrollBar()->setContextMenuPolicy(Qt::NoContextMenu);
setBackgroundBrush(QBrush(Qt::lightGray, Qt::CrossPattern));
setRenderHint(QPainter::Antialiasing);
setDragMode(RubberBandDrag);
setRubberBandSelectionMode(Qt::ContainsItemShape);
}
};
/**
* Nothing special about this class, just a helper class.
*
* A rect item has the QGraphicsItem::ItemIsSelectable QGraphicsItem::ItemIsMovable enabled.
* So you can select and move it around.
*/
class RectItem : public QGraphicsRectItem {
public:
RectItem(QGraphicsItem *parent = nullptr)
: QGraphicsRectItem(parent)
{
const double length = 10;
setFlags(ItemIsSelectable | ItemIsMovable | ItemSendsGeometryChanges);
setRect(-length/2.0, -length/2.0, length, length);
}
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = Q_NULLPTR) override
{
setBrush(isSelected() ? QBrush(Qt::gray) : Qt::NoBrush);
QGraphicsRectItem::paint(painter, option, widget);
}
protected:
QVariant itemChange(GraphicsItemChange change, const QVariant &value) override
{
switch(change) {
case ItemPositionChange: case ItemSelectedChange:
if(scene()) {
scene()->update(); // just to avoid some ugly effect occuring on the scene.
}
break;
default:
break;
}
return QGraphicsRectItem::itemChange(change, value);
}
};
/**
* A quite simple version of what a cubic Bezier curve is:
* it starts at a given point "from",
* ends at some point "to",
* having two control points (let's say "ctrlPt1" and ctrlPt2").
*
* A curve has the QGraphicsItem::ItemIsSelectable enabled.
* So you can select it.
*/
class Curve : public QGraphicsItem {
protected:
RectItem from;
RectItem ctrlPt1;
RectItem ctrlPt2;
RectItem to;
public:
Curve(QGraphicsItem *parent = nullptr)
: QGraphicsItem(parent)
{
// simple customization
setFlags(ItemIsSelectable);
// set positions
const qreal h = 100.;
const qreal d = 100.;
from.setPos(-150, 0);
ctrlPt1.setPos(from.pos() + QPointF(d, -h));
ctrlPt2.setPos(ctrlPt1.pos() + QPointF(d, 0));
to.setPos(ctrlPt2.x()+d, ctrlPt2.y()+h);
}
// Should be called after scene is defined for this item.
void addPoints() {
QList<QGraphicsRectItem*> list;
list << &from << &ctrlPt1 << &ctrlPt2 << &to;
for(auto *item : list) {
scene()->addItem(item);
}
}
QRectF boundingRect() const override
{
QPolygonF poly;
poly << from.pos() << ctrlPt1.pos() << ctrlPt2.pos() << to.pos();
return poly.boundingRect()
.normalized();
}
QPainterPath shape() const override
{
QPainterPath path;
path.moveTo(from.pos());
path.cubicTo(ctrlPt1.pos(), ctrlPt2.pos(), to.pos());
return path;
}
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = Q_NULLPTR) override
{
Q_UNUSED(option)
Q_UNUSED(widget)
// Draw curve
QPen pen = QPen(Qt::darkBlue);
pen.setWidthF(isSelected() ? 3. : 1.);
painter->setPen(pen); // curve pen
painter->setBrush(Qt::green); // curve brush
painter->drawPath(shape());
// Tie ctrl points
const bool tieCtrlPoints = from.isSelected() || ctrlPt1.isSelected() || ctrlPt2.isSelected() || to.isSelected();
if(tieCtrlPoints) {
painter->setPen(Qt::black);
painter->setBrush(Qt::black);
painter->drawLine(from.pos(), ctrlPt1.pos());
painter->drawLine(ctrlPt1.pos(), ctrlPt2.pos());
painter->drawLine(ctrlPt2.pos(), to.pos());
}
}
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QGraphicsScene scene;
scene.setSceneRect(-300, -300, 600, 600);
View view;
view.setScene(&scene);
Curve curve;
scene.addItem(&curve);
curve.addPoints();
view.show();
return a.exec();
}
Upvotes: 2
Views: 4287
Reputation: 2444
You don't need to do anything special expect return the appropriate QPainterPath from the shape
method. If the path you return is a simple, unclosed path, then you have to click exactly on that path to select. You didn't include the code for your shape
methods, but that's where the problem is. There should not be any need to play games with mouse events.
ADDITION:
The documentation doesn't say this, but the selection mechanism appears to treat the returned path from shape
as a closed path whether or not it actually is. I was able to fix this by using a stroker to return the outline:
QPainterPath shape() const override
{
QPainterPath path;
path.moveTo(from.pos());
path.cubicTo(ctrlPt1.pos(), ctrlPt2.pos(), to.pos());
QPainterPathStroker stroker;
return stroker.createStroke (path).simplified ();
}
This gives the selection behavior you want. However, this introduces a new problem because you're currently painting the return value of shape
. With this new code, the curve isn't filled. What I'd recommend is that you create a separate method that builds the path, and then have both shape
and paint
get the path from that new method. For example:
QPainterPath buildPath () const
{
QPainterPath path;
path.moveTo(from.pos());
path.cubicTo(ctrlPt1.pos(), ctrlPt2.pos(), to.pos());
return path;
}
QPainterPath shape() const override
{
QPainterPath path = buildPath();
QPainterPathStroker stroker;
return stroker.createStroke (path).simplified ();
}
And then in paint, have it call buildPath
instead of shape
. This approach is more consistent with the purpose of the shape
method. It is used for collision detection and selection, not for drawing. In fact, if you have very thin lines, it can be difficult for your users to accurately click on them, so with the stroker, you can extend the width of the outlined path to allow a few pixels of buffer around the curve. That works great, but you don't want to draw that result.
Upvotes: 2