用 C 建立自定義元素

QML 帶有豐富的視覺元素。僅使用 QML,我們可以使用這些元素構建複雜的應用程式。此外,基於標準專案(如矩形,按鈕,影象等)構建自己的元素非常容易。此外,我們可以使用像 Canvas 這樣的專案來構建具有自定義繪畫的元素。我們似乎只能在 QML 中構建各種應用程式,而不會觸及 C++的功能。它實際上是正確的,但有時我們仍希望使我們的應用程式更快,或者我們希望用 Qt 的功能擴充套件它,或者增加一些 QML 中沒有的機會。當然在 QML 中也存在這種可能性。基本上 QtQuick 使用 Scene Graph 將其內容繪製成基於的高效能渲染引擎 OpenGL 。要實現我們自己的可視元素,我們可以使用兩種方式:

  1. 傳統的 Qt 方式使用 QPainterQQuickPaintedItem )。
  2. 使用 QQuickItem 和 OpenGL 功能的常見 QML 方式。

有可能第一種方法似乎更容易,但值得考慮的是它也比第一種方法慢,因為 QtQuick 在表面上繪製專案的內容然後將其插入到場景圖中,因此渲染是兩步操作。因此,直接使用場景圖 API 總是要快得多。

為了探索兩種方法,讓我們建立我們自己的元素,這在 QML 中肯定不存在,例如三角形。

類宣告

class QQuickCustomItem : public QQuickItem
{
    Q_OBJECT
    Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged)
public:
    QQuickCustomItem(QQuickItem *parent = Q_NULLPTR);

protected:
    QSGNode *updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *updatePaintNodeData);

    QColor color() const;
    void setColor(const QColor &color);

private:
    QColor m_color;
    bool m_needUpdate;

signals:
    void colorChanged();
};

我們新增 Q_OBJECT 巨集來處理訊號。我們還新增了自定義屬性來指定 Rectangle 的顏色。為了使它工作,我們需要的是重新實現虛擬函式 QQuiclItem :: updatePaintNode()

類實現

首先,我們定義一個建構函式。

QQuickCustomItem::QQuickCustomItem(QQuickItem *parent) :
    QQuickItem(parent),
    m_color(Qt::red),
    m_needUpdate(true)
{
    setFlag(QQuickItem::ItemHasContents);
}

請注意, setFlag() 函式呼叫是必需的,否則你的物件將不會新增到場景圖中。接下來,我們定義一個痛苦的函式。

QSGNode *QQuickCustomItem::updatePaintNode(QSGNode *oldNode, QQuickItem::UpdatePaintNodeData *updatePaintNodeData)
{
    Q_UNUSED(updatePaintNodeData)
    QSGGeometryNode *root = static_cast<QSGGeometryNode *>(oldNode);

    if(!root) {
        root = new QSGGeometryNode;
        QSGGeometry *geometry = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 3);
        geometry->setDrawingMode(GL_TRIANGLE_FAN);
        geometry->vertexDataAsPoint2D()[0].set(width() / 2, 0);
        geometry->vertexDataAsPoint2D()[1].set(width(), height());
        geometry->vertexDataAsPoint2D()[2].set(0, height());

        root->setGeometry(geometry);
        root->setFlag(QSGNode::OwnsGeometry);
        root->setFlag(QSGNode::OwnsMaterial);
    }

    if(m_needUpdate) {
        QSGFlatColorMaterial *material = new QSGFlatColorMaterial;
        material->setColor(m_color);
        root->setMaterial(material);
        m_needUpdate = false;
    }

    return root;
}

在第一次呼叫函式時,我們的節點尚未建立,因此 oldNode 將為 NULL。因此,我們建立節點併為其指定幾何和材質。這裡我們使用 GL_TRIANGLE_FAN 為我們的幾何體繪製實心矩形。這一點與 OpenGL 中的相同。例如,要繪製三角形框架,我們可以將程式碼更改為:

geometry->setDrawingMode(GL_LINE_LOOP);
geometry->setLineWidth(5);

你可以參考 OpenGL 手冊來檢查其他形狀。所以,剩下的就是為我們的屬性定義 setter / getter:

QColor QQuickCustomItem::color() const
{
    return m_color;
}

void QQuickCustomItem::setColor(const QColor &color)
{
    if(m_color != color) {
        m_color = color;
        m_needUpdate = true;
        update();
        colorChanged();
    }
}

現在只有一個小細節可以使它工作。我們需要通知 QtQuick 新專案。例如,你可以將此程式碼新增到 main.cpp:

qmlRegisterType<QQuickCustomItem>("stackoverflow.qml", 1, 0, "Triangle");

這是我們的 QML 測試檔案:

import QtQuick 2.7
import QtQuick.Window 2.0
import stackoverflow.qml 1.0

Window {
    width: 800
    height: 800
    visible: true

    Rectangle {
        width: 200
        height: 200
        anchors.centerIn: parent
        color: "lightgrey"

        Triangle {
            id: rect
            width: 200
            height: 200
            transformOrigin: Item.Top
            color: "green"
            onColorChanged: console.log("color was changed");
            PropertyAnimation on rotation {
                from: 0
                to: 360
                duration: 5000
                loops: Animation.Infinite
            }
        }
    }
    Timer {
        interval: 1000
        repeat: true
        running: true
        onTriggered: rect.color = Qt.rgba(Math.random(),Math.random(),Math.random(),1);
    }
}

如你所見,我們的專案與所有其他 QML 專案一樣。現在讓我們使用 QPainter 建立相同的專案 :

我們所需要的就是更換

QSGNode *updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *updatePaintNodeData);

void paint(QPainter *painter);

並且,cource 從 QQuickPaintedItem 而不是 QQuickItem 繼承我們的類。這是我們的繪畫功能:

void QQuickCustomItem::paint(QPainter *painter)
{
    QPainterPath path;
    path.moveTo(width() / 2, 0);
    path.lineTo(width(), height());
    path.lineTo(0, height());
    path.lineTo(width() / 2, 0);
    painter->fillPath(path, m_color);
}

其他一切都保持不變。