Reallocating QSGGeometryNode Vertex Data - qt

I am using Qt 5.8. I am attempting to draw circular points using QSGGeometry::DrawTriangles (using QSGGeometry::DrawPoints draws points as rectangles/squares--not desired). In order to do this I am drawing 8 triangles that gives the illusion of a circle. Each data point will have 8 triangles associated with it. The number of data points can vary at any given time. After a (user) specified amount of time as a data point is added, one data point is removed. There seems to be an error in the allocation of data when it's drawn. I used setVertexDataPattern(QSGGeometry::StreamPattern); in the construction of the QSGGeometryNode; in hopes of getting the desired output.
On each draw call, I call m_geometry.allocate(m_pts.size() * MAX_VERTICES), where MAX_VERTICES = 24 in case the number of points since the last draw call has changed. I have attempted to use GL_POLYGON (since it would require fewer vertices), but the same problem happens. There seems to be an attempt to draw a shape from one slice of the the first data point to another slice of the last data point. Is there something wrong with reallocating for every draw call? What is the proper way to handle drawing data with varying sizes?
Update I think it may deal with a size issue. I have sample code that only draws 1 triangle (instead of 8) and once you get to about 25000 (times 3 for each triangle) the odd line appears and seems to stop drawing additional triangles. In the following sample code (when using a smaller number of points) the last triangle is white.
main.qml
import QtQuick 2.8
import QtQuick.Window 2.2
import TestModule 1.0
Window {
visible: true
width: 640
height: 480
title: qsTr("Hello World")
Rectangle {
color: "black"
width: parent.width
height: parent.height * .90
anchors.top: parent.top
MouseArea {
anchors.fill: parent
onClicked: {
Qt.quit();
}
}
TestItem {
id: testItem
anchors.fill: parent
ptCount: 25000
color: "green"
}
}
Rectangle {
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
height: parent.height * .10
width: parent.width
border.color: "pink"
color: "lightgray"
TextInput {
anchors.fill: parent
anchors.centerIn: parent
id: textInput
text: "enter max number of points here"
horizontalAlignment: TextInput.AlignHCenter
verticalAlignment: TextInput.AlignVCenter
color: "steelblue"
onEditingFinished: testItem.ptCount = parseInt(textInput.text)
validator: IntValidator{bottom: 1}
}
}
}
TestItem.h
#include <QQuickItem>
class QSGGeometryNode;
class TestItem : public QQuickItem
{
Q_OBJECT
Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged)
Q_PROPERTY(qint32 ptCount READ ptCount WRITE setPtCount NOTIFY ptCountChanged)
public:
explicit TestItem(QQuickItem *parent = 0);
QColor color();
void setColor(const QColor &color);
void setPtCount(const qint32& newVal);
qint32 ptCount();
signals:
void colorChanged();
void ptCountChanged();
protected:
QSGNode *updatePaintNode(QSGNode *, UpdatePaintNodeData *);
void geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry);
QColor m_color;
qint32 m_ptCount;
};
TestItem.cpp
#include "TestItem.h"
#include <QSGNode>
#include <QSGVertexColorMaterial>
TestItem::TestItem(QQuickItem *parent) : QQuickItem(parent), m_color(Qt::green), m_ptCount(25000)
{
setFlag(ItemHasContents, true);
}
QColor TestItem::color()
{
return m_color;
}
void TestItem::setColor(const QColor &color)
{
m_color = color;
update();
emit colorChanged();
}
void TestItem::setPtCount(const qint32 &newVal)
{
if (newVal < 0)
m_ptCount = 25000;
else
m_ptCount = newVal;
update();
emit ptCountChanged();
}
qint32 TestItem::ptCount()
{
return m_ptCount;
}
QSGNode *TestItem::updatePaintNode(QSGNode *oldNode, QQuickItem::UpdatePaintNodeData *)
{
QSGGeometryNode *node = nullptr;
QSGGeometry *geometry = nullptr;
if (!oldNode)
{
node = new QSGGeometryNode;
geometry = new QSGGeometry(QSGGeometry::defaultAttributes_ColoredPoint2D(), m_ptCount * 3);
geometry->setDrawingMode(GL_TRIANGLES);
node->setGeometry(geometry);
node->setFlag(QSGNode::OwnsGeometry);
QSGVertexColorMaterial *material = new QSGVertexColorMaterial;
node->setMaterial(material);
node->setFlag(QSGNode::OwnsMaterial);
}
else
{
node = static_cast<QSGGeometryNode *>(oldNode);
geometry = node->geometry();
geometry->allocate(m_ptCount * 3);
}
QSGGeometry::ColoredPoint2D *vertices = geometry->vertexDataAsColoredPoint2D();
qreal triWidth = 250/boundingRect().width() + 10;
for (int i = 0; i < m_ptCount; ++i)
{
QColor color;
if (i == m_ptCount - 1)
color = Qt::white;
else
color = m_color;
qreal x0 = (boundingRect().width() * .90/m_ptCount) * i ;
qreal y0 = 60 * sinf(x0* 3.14/180); // 60 just varies the height of the wave
qreal x1 = x0 + 0.05 * boundingRect().width(); // 0.05 so that we have 5% space on each side
qreal y1 = y0 + boundingRect().height()/2;
vertices[i * 3].set(x1, y1, color.red(), color.green(), color.blue(), color.alpha());
vertices[i * 3 + 1].set(x1 + triWidth, y1, color.red(), color.green(), color.blue(), color.alpha());
vertices[i * 3 + 2].set(x1 + triWidth, y1 + triWidth, color.red(), color.green(), color.blue(), color.alpha());
}
node->markDirty(QSGNode::DirtyGeometry);
return node;
}
void TestItem::geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry)
{
update();
QQuickItem::geometryChanged(newGeometry, oldGeometry);
}
Any help with determining if this is a Qt bug or my error somewhere?

It turns out I needed to change the geometry's constructor to use UnsignedIntType; it defaults to UnsignedShortType.
geometry = new QSGGeometry(QSGGeometry::defaultAttributes_ColoredPoint2D(), m_ptCount * 3, 0, QSGGeometry::UnsignedIntType);

Related

Animating MapQuickItem in QML on position updates

I have an QAbstractListModel object that maintains a list of items to show on a map. The position of these items changes every few seconds and it is easy to calculate a pretty accurate position 60 seconds in the future. What I am trying to do is to set the item's position when it gets a new position (that part works well) and then to immediately move the item toward the calculated future position.
The code without animation looks like this and it works fine:
Component {
id: drawTarget
MapQuickItem {
id: marker
coordinate: data.coords
sourceItem: Item {
id: item
...
The data object has a property which returns the estimated position of the item 60 seconds in the future, so I tried this:
Component {
id: drawTarget
MapQuickItem {
id: marker
coordinate: data.coords
CoordinateAnimation {
id:anim
property: "coordinate"
}
onCoordinateChanged: {
anim.stop()
anim.from = data.coords
anim.to = data.coordsIn60sec
anim.duration = 60000
anim.start()
}
sourceItem: Item {
id: item
...
But although the object's position is updated properly at each position update, the animation toward the future estimated position doesn't work at all.
How would one go about doing something like this?
In its code, it makes a binding coordinate: data.coords that states that "coordinate" takes the value of "coords" but at the same time says that "coordinate" depends on the animation, isn't it contradictory? Well, it is contradictory.
The idea is not to do the binding coordinate: data.coords but to update the property only through the animation.
The following code is a workable example:
main.qml
import QtQuick 2.14
import QtQuick.Window 2.14
import QtLocation 5.6
import QtPositioning 5.6
Window {
visible: true
width: 640
height: 480
Plugin {
id: mapPlugin
name: "osm"
}
Map {
anchors.fill: parent
plugin: mapPlugin
center: QtPositioning.coordinate(59.91, 10.75) // Oslo
zoomLevel: 10
MapItemView{
model: datamodel
delegate: MapQuickItem{
id: item
// begin configuration
property var position: model.position
property var nextposition: model.nextposition
onPositionChanged: restart();
onNextpositionChanged: restart();
function restart(){
anim.stop()
anim.from = position
anim.to = nextposition
anim.start()
}
CoordinateAnimation {
id: anim
target: item
duration: 60 * 1000
property: "coordinate"
}
// end of configuration
anchorPoint.x: rect.width/2
anchorPoint.y: rect.height/2
sourceItem: Rectangle{
id: rect
color: "green"
width: 10
height: 10
}
}
}
}
}
datamodel.h
#ifndef DATAMODEL_H
#define DATAMODEL_H
#include <QAbstractListModel>
#include <QGeoCoordinate>
#include <QTimer>
#include <random>
#include <QDebug>
struct Data{
QGeoCoordinate position;
QGeoCoordinate nextposition;
};
static QGeoCoordinate osloposition(59.91, 10.75); // Oslo;
class DataModel : public QAbstractListModel
{
Q_OBJECT
QList<Data> m_datas;
public:
enum PositionRoles {
PositionRole = Qt::UserRole + 1,
NextPositionRole
};
explicit DataModel(QObject *parent = nullptr)
: QAbstractListModel(parent)
{
init();
}
int rowCount(const QModelIndex &parent = QModelIndex()) const override{
return parent.isValid() ? 0: m_datas.count();
}
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override{
if (!index.isValid() || index.row() < 0 || index.row() >= m_datas.count())
return QVariant();
const Data &data = m_datas[index.row()];
if (role == PositionRole)
return QVariant::fromValue(data.position);
else if (role == NextPositionRole)
return QVariant::fromValue(data.nextposition);
return QVariant();
}
QHash<int, QByteArray> roleNames() const override{
QHash<int, QByteArray> roles;
roles[PositionRole] = "position";
roles[NextPositionRole] = "nextposition";
return roles;
}
private:
void init(){
for(int i=0; i< 10; ++i){
Data data;
data.position = osloposition;;
data.nextposition = data.position;
m_datas << data;
}
QTimer *timer = new QTimer(this);
QObject::connect(timer, &QTimer::timeout, this, &DataModel::updateData);
timer->start(60 * 1000);
updateData();
}
void updateData(){
qDebug() << __PRETTY_FUNCTION__;
static std::default_random_engine e;
static std::uniform_real_distribution<> dis(-.1, .1);
for(int i=0; i < m_datas.count(); ++i){
Data & data = m_datas[i];
QModelIndex ix = index(i);
data.position = data.nextposition;
data.nextposition = QGeoCoordinate(osloposition.latitude() + dis(e),
osloposition.longitude() + dis(e));
Q_EMIT dataChanged(ix, ix, {PositionRole, NextPositionRole});
}
}
};
#endif // DATAMODEL_H
In the following link is the complete example

QML Clip Custom Item

I made my own QQuickItem, which I draw using the updatePaintNode method (using the scenegraph). In a qml File I add this item as a child to a Rectangle:
Rectangle {
anchors.centerIn: parent
width: parent.width/2.
height: parent.height/2.
border.color: "#000000"
clip: true
MyItem {
id: myitem
anchors.centerIn: parent
width: parent.width
height: parent.height
MouseArea {
anchors.fill: parent
onWheel: {
if (wheel.modifiers & Qt.ControlModifier) {
parent.scale = parent.scale+ 0.2 * wheel.angleDelta.y / 120;
if (parent.scale < 1)
parent.scale = 1;
}
}
}
}
}
As you can see, I added a zoom function and set
clip: True
for the rectangle. However, when I zoom, my item is not clipped at the Rectangle but extends beyond it. What would be the correct way to do clipping?
Edit: Here is the content of the cpp file for MyItem
MyItem::MyItem(QQuickItem *parent): QQuickItem(parent)
{
setFlag(QQuickItem::ItemHasContents,true);
// code to initialize xdata, ydata, xmax, ymax
}
QSGNode *MyItem::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) {
QSGGeometryNode *mypath = static_cast<QSGGeometryNode*>(oldNode);
if(!mypath) {
mypath = new QSGGeometryNode;
mypath->setFlag(QSGNode::OwnsMaterial,true);
mypath->setFlag(QSGNode::OwnsGeometry,true);
}
QSGGeometry *geometry = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(),_xdata.size());
QSGGeometry::Point2D *points = geometry->vertexDataAsPoint2D();
for(int i=0;i<_xdata.size();i++) {
points[i].x = _xdata[i]/_xmax*width();
points[i].y = _ydata[i]/_ymax*height();
}
geometry->setLineWidth(2);
geometry->setDrawingMode(GL_LINE_STRIP);
mypath->setGeometry(geometry);
QSGFlatColorMaterial *material = new QSGFlatColorMaterial;
material->setColor(Qt::blue);
mypath->setMaterial(material);
return mypath;
}
and here is the header file:
#include <QQuickItem>
#include <vector>
class MyItem : public QQuickItem
{
Q_OBJECT
public:
MyItem(QQuickItem *parent = 0);
private:
std::vector<double> _xdata;
std::vector<double> _ydata;
double _xmax;
double _ymax;
signals:
public slots:
protected:
QSGNode * updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *data) override;
};

Drawing QQuickItem's texture in a "draw-chain" through 2 QQuickFramebufferObjects fails

First off, sorry for the long source code but I couldn't reduce it any further while still having the bug happen. Most of it is uninteresting boilerplate though, so you can mostly get away with just my human-readable summary below.
Abbreviations used in this post:
QQFBO = QQuickFramebufferObject
Observed result: Two rotating squares, that's all.
What's missing from the observed result: A third, additional, non-rotating square, which is actually a static snapshot from the second one.
What my code does, in short:
class ClonerItem is a QQFBO just fetches a texture from a QQuickItem and draws it to the screen.
The ClonerItemQml is a Qml wrapper of ClonerItem that adds a Timer to it. Depending on the ClonerItem.live property, the timer either calls ClonerItem.update() constantly or not at all. (basically the same the live property of Qt's ShaderEffectSource)
In main.qml I create a "producer-consumer-chain" of 3 items in a RowLayout.
The first item is a rotating rectangle with layer.enabled: true.
The second item is a ClonerItemQml (with live: true) that has the previous one as a texture source.
The third item is a ClonerItemQml (called "snapshotter", with live: false) that has the the previous one as a texture source.
"snapshotter" has its update method connected to 2 signals:
A firstRedraw signal provided by item "2", which is called at the end of the first redraw of item "2"
For testing, the clicked signal for "snapshotter", so you can see that the "snapshotter" does work correctly when you force it to update later during the program's execution.
Any hints what I'm doing wrong?
Note: When I remove the Timer and instead of it, uncomment the if(m_live) update(); in render, it works fine.
My code:
main.cpp:
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQuickFramebufferObject>
#include <QOpenGLShaderProgram>
#include <QOpenGLFunctions>
#include <QOpenGLFramebufferObject>
#include <QQuickWindow>
// propertyhelper.h is from http://syncor.blogspot.bg/2014/11/qt-auto-property.html
#include "propertyhelper.h"
#include <QSGTextureProvider>
class ClonerItem : public QQuickFramebufferObject {
Q_OBJECT
AUTO_PROPERTY(QQuickItem*, sourceItem)
AUTO_PROPERTY(bool, live)
public:
Renderer* createRenderer() const;
signals:
void firstRedraw();
};
class ClonerItemRenderer : public QObject, public QQuickFramebufferObject::Renderer, protected QOpenGLFunctions {
Q_OBJECT
public:
ClonerItemRenderer() {
initializeOpenGLFunctions();
m_program.addShaderFromSourceFile(QOpenGLShader::Vertex, ":/shader.vert.glsl");
m_program.addShaderFromSourceFile(QOpenGLShader::Fragment, ":/shader.frag.glsl");
m_program.link();
createGeometry();
}
void render() {
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
glClear(GL_COLOR_BUFFER_BIT);
glDisable(GL_CULL_FACE);
glDisable(GL_DEPTH_TEST);
m_program.bind();
glActiveTexture(GL_TEXTURE0);
m_tex->bind();
m_program.setUniformValue("uTex", 0);
paintGeometry();
glFlush();
glFinish();
m_window->resetOpenGLState();
if(!m_haveRedrawnAtLeastOnce)
emit firstRedraw();
m_haveRedrawnAtLeastOnce = true;
//if(m_live)
// update();
}
void paintGeometry() {
m_program.enableAttributeArray("aPos");
m_program.setAttributeArray("aPos", m_vertices.constData());
glDrawArrays(GL_TRIANGLES, 0, m_vertices.size());
}
void createGeometry() {
m_vertices << QVector2D(-1, -1) << QVector2D(-1, +1) << QVector2D(+1, +1);
m_vertices << QVector2D(-1, -1) << QVector2D(+1, -1) << QVector2D(+1, +1);
}
QOpenGLFramebufferObject* createFramebufferObject(const QSize &size) {
QOpenGLFramebufferObjectFormat format;
format.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil);
return new QOpenGLFramebufferObject(size, format);
}
private:
QVector<QVector2D> m_vertices;
QOpenGLShaderProgram m_program;
QQuickWindow* m_window;
QSGTexture* m_tex;
bool m_haveRedrawnAtLeastOnce = false;
bool m_live;
protected:
void synchronize(QQuickFramebufferObject* qqfbo) {
ClonerItem* parentItem = (ClonerItem*)qqfbo;
m_window = parentItem->window();
m_live = parentItem->live();
// fetch texture pointer
QQuickItem* sourceItem = parentItem->sourceItem();
QSGTextureProvider* sourceTexProvider = sourceItem->textureProvider();
m_tex = sourceTexProvider->texture();
}
signals:
void firstRedraw();
};
QQuickFramebufferObject::Renderer* ClonerItem::createRenderer() const {
auto renderer = new ClonerItemRenderer();
connect(renderer, &ClonerItemRenderer::firstRedraw, this, &ClonerItem::firstRedraw);
return renderer;
}
int main(int argc, char **argv) {
QGuiApplication app(argc, argv);
qmlRegisterType<ClonerItem>("ClonerItem", 1, 0, "ClonerItem");
QQmlApplicationEngine engine;
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
return app.exec();
}
#include "main.moc"
ClonerItem.qml:
import QtQuick 2.0
import ClonerItem 1.0
ClonerItem {
id: root
Timer {
id: renderTimer
running: root.live
interval: 1000/60
repeat: true
onTriggered: {
root.update();
}
}
}
main.qml:
import QtQuick 2.0
import ClonerItem 1.0
import QtQuick.Window 2.2
import QtQuick.Layouts 1.3
Window {
visible: true
width: 600
height: 200
RowLayout {
id: rowLayout
anchors.fill: parent
Item {
id: original
layer.enabled: true
Layout.fillWidth: true
Layout.fillHeight: true
RotatingSquare { }
}
ClonerItemQml {
id: liveClone
layer.enabled: true
sourceItem: original
live: true
Layout.fillWidth: true
Layout.fillHeight: true
onFirstRedraw: {
snapshotter.update();
}
}
ClonerItemQml {
id: snapshotter
sourceItem: liveClone
live: false
Layout.fillWidth: true
Layout.fillHeight: true
MouseArea {
anchors.fill: parent
onClicked: {
snapshotter.update();
}
}
}
}
}
RotatingSquare.qml:
import QtQuick 2.0
Rectangle {
color: "red"
border.color: "black"
width: parent.width / 2
height: parent.height / 2
anchors.centerIn: parent
NumberAnimation on rotation {
running: true
loops: Animation.Infinite
duration: 1000
from: 0
to: 360
}
}
shader.frag.glsl:
varying highp vec2 vTexCoord;
uniform sampler2D uTex;
void main() {
gl_FragColor = texture2D(uTex, vTexCoord);
}
shader.vert.glsl
attribute highp vec2 aPos;
varying highp vec2 vTexCoord;
void main() {
gl_Position = vec4(aPos, 0.0, 1.0);
// aPos's components are in [-1, 1], transform them to the range [0, 1] for use as texcoords.
vTexCoord = aPos * .5 + vec2(.5);
}
If you're wondering why I'm doing such an elaborate/weird setup, I can elucidate: in my real app the second item in the "chain" is not a simple "cloner" but actually uses its source texture(s) for some more-complex rendering (which is also memory-hungry). And the third item in the "chain" is necessary to snapshot the result of the second one, so the second one can be destroyed when not animated, freeing up memory.
The reason I implement my own ClonerItem component, which does basically the same as Qt's ShaderEffectSource, is this Qt bug.

How to create a round mouse area in QML

I have a basic custom button using a Rectangle with radius: width/2. Now I add a MouseArea to my button. However the MouseArea has a squared shape. That means the click event is also triggered when I click slightly outside the round button, i.e. in the corners of the imaginary square around the round button. Can I somehow make also the MouseArea round?
import QtQuick 2.7
import QtQuick.Window 2.2
Window {
visible: true
width: 640
height: 480
title: qsTr("TestApp")
Rectangle {
id: background
anchors.fill: parent
color: Qt.rgba(0.25, 0.25, 0.25, 1);
Rectangle {
id: button
width: 64
height: 64
color: "transparent"
anchors.centerIn: parent
radius: 32
border.width: 4
border.color: "grey"
MouseArea {
anchors.fill: parent
onPressed: button.color = "red";
onReleased: button.color = "transparent";
}
}
}
}
Stealing code from PieMenu, here's RoundMouseArea.qml:
import QtQuick 2.0
Item {
id: roundMouseArea
property alias mouseX: mouseArea.mouseX
property alias mouseY: mouseArea.mouseY
property bool containsMouse: {
var x1 = width / 2;
var y1 = height / 2;
var x2 = mouseX;
var y2 = mouseY;
var distanceFromCenter = Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2);
var radiusSquared = Math.pow(Math.min(width, height) / 2, 2);
var isWithinOurRadius = distanceFromCenter < radiusSquared;
return isWithinOurRadius;
}
readonly property bool pressed: containsMouse && mouseArea.pressed
signal clicked
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: if (roundMouseArea.containsMouse) roundMouseArea.clicked()
}
}
You can use it like this:
import QtQuick 2.5
import QtQuick.Window 2.2
Window {
width: 640
height: 480
visible: true
RoundMouseArea {
id: roundMouseArea
width: 100
height: 100
anchors.centerIn: parent
onClicked: print("clicked")
// Show the boundary of the area and whether or not it's hovered.
Rectangle {
color: roundMouseArea.pressed ? "red" : (roundMouseArea.containsMouse ? "darkorange" : "transparent")
border.color: "darkorange"
radius: width / 2
anchors.fill: parent
}
}
}
Another option is a C++/QML way as decribed in this example. This example provides a way to use masks of any shapes. It can be customized to fit your needs.
Posting the code as it is:
maskedmousearea.cpp
MaskedMouseArea::MaskedMouseArea(QQuickItem *parent)
: QQuickItem(parent),
m_pressed(false),
m_alphaThreshold(0.0),
m_containsMouse(false)
{
setAcceptHoverEvents(true);
setAcceptedMouseButtons(Qt::LeftButton);
}
void MaskedMouseArea::setPressed(bool pressed)
{
if (m_pressed != pressed) {
m_pressed = pressed;
emit pressedChanged();
}
}
void MaskedMouseArea::setContainsMouse(bool containsMouse)
{
if (m_containsMouse != containsMouse) {
m_containsMouse = containsMouse;
emit containsMouseChanged();
}
}
void MaskedMouseArea::setMaskSource(const QUrl &source)
{
if (m_maskSource != source) {
m_maskSource = source;
m_maskImage = QImage(QQmlFile::urlToLocalFileOrQrc(source));
emit maskSourceChanged();
}
}
void MaskedMouseArea::setAlphaThreshold(qreal threshold)
{
if (m_alphaThreshold != threshold) {
m_alphaThreshold = threshold;
emit alphaThresholdChanged();
}
}
bool MaskedMouseArea::contains(const QPointF &point) const
{
if (!QQuickItem::contains(point) || m_maskImage.isNull())
return false;
QPoint p = point.toPoint();
if (p.x() < 0 || p.x() >= m_maskImage.width() ||
p.y() < 0 || p.y() >= m_maskImage.height())
return false;
qreal r = qBound<int>(0, m_alphaThreshold * 255, 255);
return qAlpha(m_maskImage.pixel(p)) > r;
}
void MaskedMouseArea::mousePressEvent(QMouseEvent *event)
{
setPressed(true);
m_pressPoint = event->pos();
emit pressed();
}
void MaskedMouseArea::mouseReleaseEvent(QMouseEvent *event)
{
setPressed(false);
emit released();
const int threshold = qApp->styleHints()->startDragDistance();
const bool isClick = (threshold >= qAbs(event->x() - m_pressPoint.x()) &&
threshold >= qAbs(event->y() - m_pressPoint.y()));
if (isClick)
emit clicked();
}
void MaskedMouseArea::mouseUngrabEvent()
{
setPressed(false);
emit canceled();
}
void MaskedMouseArea::hoverEnterEvent(QHoverEvent *event)
{
Q_UNUSED(event);
setContainsMouse(true);
}
void MaskedMouseArea::hoverLeaveEvent(QHoverEvent *event)
{
Q_UNUSED(event);
setContainsMouse(false);
}
Usage in QML:
import Example 1.0
MaskedMouseArea {
id: moonArea
anchors.fill: parent
alphaThreshold: 0.4
maskSource: moon.source
}
Register the custom item:
qmlRegisterType<MaskedMouseArea>("Example", 1, 0, "MaskedMouseArea");
Thanks to #Mitch. Sometimes such mousearea says it contains mouse after leaving it, so I've added "if(!mouseArea.containsMouse) return false;" to the beginning of the "containsMouse" property:
property bool containsMouse: {
if(!mouseArea.containsMouse)
return false;
var x1 = width / 2;
var y1 = height / 2;
var x2 = mouseX;
var y2 = mouseY;
var distanceFromCenter = Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2);
var radiusSquared = Math.pow(Math.min(width, height) / 2, 2);
var isWithinOurRadius = distanceFromCenter < radiusSquared;
return isWithinOurRadius;
}

How to integrate a custom GraphicsItem into a QML scene?

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?
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).

Resources