How to integrate a custom GraphicsItem into a QML

2019-07-24 20:11发布

问题:

Assume you have created the following custom QGraphicsRectItem in C++:

class MyCustomItem : public QGraphicsRectItem
{
  public:
    MyCustomItem(MyCustomItem* a_Parent = 0);
    virtual ~MyCustomItem();

    // specific methods

  private:
    // specific data
};

Assume also that you have defined in a QML script an ApplicationWindow:

// main.qml
import QtQuick 2.4
import QtQuick.Controls 1.3
import QtQuick.Window 2.0

ApplicationWindow {
    id: myWindow
    title: qsTr("My Window")
    width: 640
    height: 480
    visible: true
}

The simple task I would like to do is to display an instance of MyCustomItem in that ApplicationWindow. I wanted to do the following:

// part of main.cpp
int main(int argc, char *argv[])
{
    QApplication app(argc, argv);

    QQmlApplicationEngine engine;
    engine.load(QUrl(QStringLiteral("qrc:/main.qml")));

    MyCustomItem* myItem;
    engine.rootContext()->setContextProperty("MyCustomItem", myItem);

    return app.exec();
}

But of course this doesn't work, because MyCustomItem is neither a QObject nor a QVariant. I don't want my item to be anything else than a QGraphicsRectItem. Isn't that possible to display that graphics item? That should be simple as hell, shouldn't it? Is there a way with QDeclarativeItem or something? I can't find how to solve this problem, that's very frustrating. Would I implement my application with "normal" Qt, the problem would already be solved, because in this case you have a scene, and the scene has a member method addItem() and I don't need to do complicated stuff to add my custom graphics item to my scene. Do I have to wrap this item in a QDeclarativeItem or a QObject in order to get the thing done? That would be so awful, in my opinion. Aren't there better options?

EDIT

Can that be that QGraphicsRectItem is not the right class to inherit from and that something like QQuickPaintedItem (as suggested in the comments) would be more appropriate?

回答1:

I can't speak for Qt 4, but in Qt 5, you have several options for custom drawing:

QQuickPaintedItem

A QPainter-based QQuickItem. This sounds the closest to what you want. A snippet from the documentation of one of the examples:

void TextBalloon::paint(QPainter *painter)
{
    QBrush brush(QColor("#007430"));

    painter->setBrush(brush);
    painter->setPen(Qt::NoPen);
    painter->setRenderHint(QPainter::Antialiasing);

    painter->drawRoundedRect(0, 0, boundingRect().width(), boundingRect().height() - 10, 10, 10);

    if (rightAligned)
    {
        const QPointF points[3] = {
            QPointF(boundingRect().width() - 10.0, boundingRect().height() - 10.0),
            QPointF(boundingRect().width() - 20.0, boundingRect().height()),
            QPointF(boundingRect().width() - 30.0, boundingRect().height() - 10.0),
        };
        painter->drawConvexPolygon(points, 3);
    }
    else
    {
        const QPointF points[3] = {
            QPointF(10.0, boundingRect().height() - 10.0),
            QPointF(20.0, boundingRect().height()),
            QPointF(30.0, boundingRect().height() - 10.0),
        };
        painter->drawConvexPolygon(points, 3);
    }
}

Canvas

JavaScript-based drawing QML type with an HTML5-like API. A snippet from one of the examples:

Canvas {
    id: canvas
    width: 320
    height: 250
    antialiasing: true

    property color strokeStyle: Qt.darker(fillStyle, 1.2)
    property color fillStyle: "#6400aa"

    property int lineWidth: 2
    property int nSize: nCtrl.value
    property real radius: rCtrl.value
    property bool fill: true
    property bool stroke: false
    property real px: width/2
    property real py: height/2 + 10
    property real alpha: 1.0

    onRadiusChanged: requestPaint();
    onLineWidthChanged: requestPaint();
    onNSizeChanged: requestPaint();
    onFillChanged: requestPaint();
    onStrokeChanged: requestPaint();

    onPaint: squcirle();

    function squcirle() {
        var ctx = canvas.getContext("2d");
        var N = canvas.nSize;
        var R = canvas.radius;

        N=Math.abs(N);
        var M=N;
        if (N>100) M=100;
        if (N<0.00000000001) M=0.00000000001;

        ctx.save();
        ctx.globalAlpha =canvas.alpha;
        ctx.fillStyle = "white";
        ctx.fillRect(0, 0, canvas.width, canvas.height);

        ctx.strokeStyle = canvas.strokeStyle;
        ctx.fillStyle = canvas.fillStyle;
        ctx.lineWidth = canvas.lineWidth;

        ctx.beginPath();
        var i = 0, x, y;
        for (i=0; i<(2*R+1); i++){
            x = Math.round(i-R) + canvas.px;
            y = Math.round(Math.pow(Math.abs(Math.pow(R,M)-Math.pow(Math.abs(i-R),M)),1/M)) + canvas.py;

            if (i == 0)
                ctx.moveTo(x, y);
            else
                ctx.lineTo(x, y);
        }

        for (i=(2*R); i<(4*R+1); i++){
            x =Math.round(3*R-i)+canvas.px;
            y = Math.round(-Math.pow(Math.abs(Math.pow(R,M)-Math.pow(Math.abs(3*R-i),M)),1/M)) + canvas.py;
            ctx.lineTo(x, y);
        }
        ctx.closePath();
        if (canvas.stroke) {
            ctx.stroke();
        }

        if (canvas.fill) {
            ctx.fill();
        }
        ctx.restore();
    }
}

QSGGeometryNode

As mentioned in this answer, you could take advantage of the Qt Quick Scene Graph. A snippet from one of the examples:

QSGNode *BezierCurve::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *)
{
    QSGGeometryNode *node = 0;
    QSGGeometry *geometry = 0;

    if (!oldNode) {
        node = new QSGGeometryNode;
        geometry = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), m_segmentCount);
        geometry->setLineWidth(2);
        geometry->setDrawingMode(GL_LINE_STRIP);
        node->setGeometry(geometry);
        node->setFlag(QSGNode::OwnsGeometry);
        QSGFlatColorMaterial *material = new QSGFlatColorMaterial;
        material->setColor(QColor(255, 0, 0));
        node->setMaterial(material);
        node->setFlag(QSGNode::OwnsMaterial);
    } else {
        node = static_cast<QSGGeometryNode *>(oldNode);
        geometry = node->geometry();
        geometry->allocate(m_segmentCount);
    }

    QRectF bounds = boundingRect();
    QSGGeometry::Point2D *vertices = geometry->vertexDataAsPoint2D();
    for (int i = 0; i < m_segmentCount; ++i) {
        qreal t = i / qreal(m_segmentCount - 1);
        qreal invt = 1 - t;

        QPointF pos = invt * invt * invt * m_p1
                    + 3 * invt * invt * t * m_p2
                    + 3 * invt * t * t * m_p3
                    + t * t * t * m_p4;

        float x = bounds.x() + pos.x() * bounds.width();
        float y = bounds.y() + pos.y() * bounds.height();

        vertices[i].set(x, y);
    }
    node->markDirty(QSGNode::DirtyGeometry);

    return node;
}

QQuickWidget

If you really want to use QGraphicsItem subclasses, you could go the opposite direction, and have a widget-based app that contains certain "Qt Quick Widgets", though this is not optimal (see Qt Weekly #16: QQuickWidget for more information).