c++ - Qt - QGraphics(形状)项目的选择

标签 c++ qt

[1/2] 背景

您好! Qt 为我们提供了创建高度自定义图形项目的方法。我们需要做的就是继承自 QGraphicsItem 并覆盖纯虚拟的 boundingRect() 函数。此外,我们可以选择性地重写虚拟 shape() 函数,以(除其他外)为项目提供更准确的形状...

现在让我们看看我用一个软件(个人学生项目)绘制的图表,我一直在用 C++ 中的 Qt 开发。

a quite beautiful graph

然后让我们用灰色突出显示上图中每条边的边界矩形。

highlighting the edge bounding rectangle

[2/2] 问题&评论

我希望项目可以选择,所以我启用了选择标志:

setFlag(ItemIsSelectable, true);

它对矩形和圆形项目来说就像一个梦想。它也适用于边缘,但不像魅力。如果我在边界矩形(上图中的灰色矩形)定义的区域中单击,确实仍然选择了边缘。 有没有办法确保仅当我们单击定义项目形状的曲线时才考虑鼠标单击事件?

我已经覆盖了所有 mouse*Event 并在 shape() 不与 event.scenePos() 相交时返回,但结果并不好。有没有办法实现我想做的事情? 是否有类似 Qt 的方法来检查鼠标位置是否在曲线路径内?

实际上我最终设置了一个标志以便边缘忽略鼠标按钮:

setAcceptedMouseButtons(Qt::NoButton);

但如果有人遇到类似的问题并且有解决方案可以分享,我会很高兴。

编辑

这是您可以编译和执行的部分代码。提醒一下,我希望仅当我们单击定义其形状的路径时才选择边(曲线)。

/**
 * 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();
}

最佳答案

您不需要做任何特别的事情,只需从 shape 方法返回适当的 QPainterPath 即可。如果您返回的路径是一个简单的、未闭合的路径,那么您必须准确地单击该路径以进行选择。您没有包含 shape 方法的代码,但这就是问题所在。应该不需要玩鼠标事件的游戏。

补充:

文档没有这样说,但选择机制似乎将 shape 返回的路径视为封闭路径,无论它是否确实如此。我能够通过使用 stroker 返回轮廓来解决这个问题:

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 ();
}

这提供了您想要的选择行为。但是,这引入了一个新问题,因为您当前正在绘制 shape 的返回值。使用这个新代码,曲线不会被填充。我建议您创建一个单独的方法来构建路径,然后让 shapepaint 从该新方法中获取路径。例如:

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 ();
}

然后在绘画中,让它调用 buildPath 而不是 shape。这种方法更符合 shape 方法的目的。它用于碰撞检测和选择,而不是用于绘图。事实上,如果您的线条非常细,您的用户可能很难准确地点击它们,因此使用描边器,您可以扩展轮廓路径的宽度,以便在曲线周围留出几个像素的缓冲区。这很好用,但你不想绘制那个结果。

关于c++ - Qt - QGraphics(形状)项目的选择,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/42215622/

相关文章:

c++ - 在 C++ API 中支持 Z3 的浮点理论

c++ - 编译按需可执行文件

QTreeView/QFileSystemModel : How do I limit tree depth?

c++ - Qt匿名信号

android - 关于纯用 c++ 编写 android 应用程序(不是混合 java/c++ 应用程序)的良好初学者 Material

c++ - 在 Windows 上删除 C++ 中的权限

c++ - CMake:使用相同的代码但包含不同的包含路径构建多个库

c++ - SetWindowsHookEx 不注入(inject) DLL

c++ - Qt 在 Windows 中部署应用程序需要的 dll

python - shared_ptr<QCoreApplication> 崩溃