drawing with Canvas in realtime (Qt/QML) - qt

I hope i will be as clear as possible for my problem.
I'm currently devlopping an Application using a QML file as GUI.
The goal is simple : i become a large amount of data via QTcpSocket (raw data from a linear camera) and I would like to display the image. It's important to know that each time I receive a frame, it's a 1*2048 array of colors. I'd like to display it as fast as possible after receiving a data.
I've tried to set up a property string containing the color and 2 others properties containing the position (x;y) and then :
Canvas {
id: camera
objectName: "cameradrawer"
x: 1270
y: 30
width: 650
height: 500
renderStrategy: Canvas.Threaded
property string iColor
property int imageX
property int imageY
property var line
property var ctx
onPaint: {
ctx = getContext('2d');
ctx.clearRect(0, 0, width, height);
// draw here
ctx.fillStyle = iColor;
ctx.fillRect(imageY,imageX, 1, 1);
}
}
When i change the properties and then use ìnvokeMethod("requestPaint"), the application keeps crashing.
Am i doing something wrong or is the QML not used for high speed processes?
Thank you in advance for your help!
PS : i can post more code (C++ part) if necessary.

I had a bit of free time so I researched the issue as I found it quite interesting. So the faster way to implement that I guess is creating an image from the data and so create a texture from it that can be used in a QQuickItem-based item.
The custom item:
CustomItem.h
class CustomItem : public QQuickItem
{
Q_OBJECT
QML_ELEMENT
public:
CustomItem(QQuickItem *parent = nullptr);
~CustomItem();
QSGNode *updatePaintNode(QSGNode *node, UpdatePaintNodeData *) override;
void componentComplete() override;
protected:
void createImage();
void timerHandler();
private:
uint32_t m_buffer[BUFFER_SIZE] = {};
QSGTexture *m_texture = nullptr;
QSGTexture *m_newTexture = nullptr;
QTimer m_timer;
};
CustomItem.cpp
CustomItem::CustomItem(QQuickItem *parent):
QQuickItem(parent)
{
setFlag( QQuickItem::ItemHasContents, true);
QObject::connect(&m_timer, &QTimer::timeout, this, &CustomItem::timerHandler);
}
CustomItem::~CustomItem()
{
if(m_texture != nullptr)
{
delete m_texture;
m_texture = nullptr;
}
if(m_newTexture != nullptr)
{
delete m_newTexture;
m_newTexture = nullptr;
}
}
QSGNode *CustomItem::updatePaintNode(QSGNode *node, UpdatePaintNodeData *)
{
QSGSimpleTextureNode *n = static_cast<QSGSimpleTextureNode *>(node);
if (n == nullptr)
{
if(m_newTexture != nullptr)
{
n = new QSGSimpleTextureNode();
n->setRect(boundingRect());
}
}
if(n != nullptr)
{
if(m_newTexture != nullptr)
{
if(m_texture != nullptr)
{
delete m_texture;
m_texture = nullptr;
}
m_texture = m_newTexture;
m_newTexture = nullptr;
n->setTexture(m_texture);
}
}
return n;
}
void CustomItem::componentComplete()
{
createImage();
m_timer.start(1000);
QQuickItem::componentComplete();
}
void CustomItem::createImage()
{
if(m_newTexture == nullptr)
{
QRandomGenerator::global()->fillRange(m_buffer, BUFFER_SIZE);
QImage img(reinterpret_cast<uchar *>(m_buffer), IMAGE_WIDTH, IMAGE_HEIGHT, QImage::Format_ARGB32);
auto wnd = window();
if(wnd != nullptr)
{
m_newTexture = wnd->createTextureFromImage(img);
}
}
}
void CustomItem::timerHandler()
{
createImage();
update();
}
Some short clarification:
the class contains a big array of integers. I simulate the periodical data updating with timer so the array is updated once per second with random data.
as soon as data changed I create a texture. I use 2 pointers to avoid deleting the old texture while rendering and so I delete the old one inside updatePaintNode when it's safety. I use QSGSimpleTextureNode since that allows use textures but I guess you can use any other classes you want. The texture has alpha channel, you can use Format_RGB32 instead to avoid that. If the item bigger the the texture it will be scaled, you can play with that too.
The main.qml for testing can be like this:
import QtQuick
import QtQuick.Window
import CustomItems 1.0
Window {
id: window
visible: true
height: 400
width: 400
Rectangle {
width: 300
height: 300
color: "orange"
anchors.centerIn: parent
CustomItem {
width: 200
height: 200
anchors.centerIn: parent
}
}
}
To register the custom item in case of CMake the following lines should be added into CMakeFiles.txt:
set(CMAKE_AUTOMOC ON)
qt_add_qml_module(qml_test
URI CustomItems
VERSION 1.0
QML_FILES main.qml
SOURCES CustomItem.h CustomItem.cpp
The sources could be found here

Related

Poor rendering quality in custom QQuickPaintedItem

I have a created a small drawing application in QML, I created a small subclass of QQuickPaintedItem. Then in QML, I used a MouseArea to feed the input to my class. From there I simply store the mouse positions in a vector and then paint the points as I receive onto a QImage using QPainter (I used a simple algorithm to draw a quadratic bezier curve using the last three points in my vector). Then I call QQuickPainted::update() and in my implementation of QQuickPaintedItem::paint() I draw the image. Now the program works ok, but the problem is that the rendering of the painting is quite poor (I am already using QPainter::AntiAliasing). Below there is a picture. As you can see the curves are not very sharp and I can see the "pixels" on oblique lines (when I try the same thing with OneNote everything is smooth and nice).
Here is the a full example from my github repository if you want to test it out (the code is below as well). Is there something I can do about this?
.
#ifndef DRAWINGCANVAS_H
#define DRAWINGCANVAS_H
#include <QObject>
#include <QQuickPaintedItem>
#include <QPixmap>
#include <QPainter>
struct Outline{
QPolygonF points;
void addPoint(QPointF p){
points.append(p);
}
void clear(){
points.clear();
}
};
// a custom QQuickPainted used as a canvas in QML
class DrawingCanvas : public QQuickPaintedItem
{
Q_OBJECT
Q_PROPERTY(bool drawing READ drawing WRITE setDrawing NOTIFY drawingChanged)
Q_PROPERTY(int penWidth READ penWidth WRITE setPenWidth NOTIFY penWidthChanged)
Q_PROPERTY(QString penColor READ penColor WRITE setPenColor NOTIFY penColorChanged)
public:
explicit DrawingCanvas(QQuickItem *parent = nullptr);
bool drawing() const;
Q_INVOKABLE void initiateBuffer();
Q_INVOKABLE void penPressed(QPointF pos);
Q_INVOKABLE void penMoved(QPointF pos);
Q_INVOKABLE void penReleased();
int penWidth() const;
void paint(QPainter *painter) override;
QString penColor() const;
public slots:
void setDrawing(bool drawing);
void setPenWidth(int penWidth);
void setPenColor(QString penColor);
signals:
void drawingChanged(bool drawing);
void penWidthChanged(int penWidth);
void penColorChanged(QString penColor);
private:
void drawOnBuffer(QPointF pos);
bool m_drawing;
QPixmap m_buffer;
int m_penWidth;
QString m_penColor;
QPointF m_lastPoint;
Outline m_currentOutline;
QRect m_updateRect;
QVector<Outline> m_outlines;
bool m_outlineEraser;
};
#endif // DRAWINGCANVAS_H
#include "drawingcanvas.h"
#include <QPainter>
DrawingCanvas::DrawingCanvas(QQuickItem *parent) : QQuickPaintedItem(parent)
{
m_penWidth = 4;
}
bool DrawingCanvas::drawing() const
{
return m_drawing;
}
void DrawingCanvas::penPressed(QPointF pos)
{
setDrawing(true);
m_currentOutline.addPoint(pos);
m_lastPoint = pos;
}
void DrawingCanvas::penMoved(QPointF pos)
{
if(drawing()){
m_currentOutline.addPoint(pos);
// draw the points on the buffer
drawOnBuffer(pos);
}
m_lastPoint = pos;
}
void DrawingCanvas::penReleased()
{
setDrawing(false);
m_outlines.append(m_currentOutline);
m_currentOutline.clear();
m_lastPoint = QPointF();
}
// draws the actual item
void DrawingCanvas::paint(QPainter *painter)
{
painter->setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform);
QPen pen;
pen.setWidth(penWidth());
pen.setColor(penColor());
painter->setPen(pen);
painter->drawPixmap(m_updateRect, m_buffer, m_updateRect);
m_updateRect = QRect();
}
// draws on the image
void DrawingCanvas::drawOnBuffer(QPointF pos)
{
QPainter bufferPainter;
if(bufferPainter.begin(&m_buffer)){
QPen pen;
pen.setWidth(penWidth());
pen.setColor(penColor());
bufferPainter.setPen(pen);
bufferPainter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform);
int pointsLength = m_currentOutline.points.length();
QPainterPath path;
// this will help smoothing the curves
if(pointsLength > 2){
auto previousPoint = m_currentOutline.points.at(pointsLength - 3);
auto mid1 = (m_lastPoint + previousPoint)/2;
auto mid2 = (pos + m_lastPoint)/2;
path.moveTo(mid1);
path.quadTo(m_lastPoint, mid2);
bufferPainter.drawPath(path);
}
// update the canvas
int rad = (penWidth() / 2) + 2;
auto dirtyRect = path.boundingRect().toRect().normalized()
.adjusted(-rad, -rad, +rad, +rad);
// change the canvas dirty region
if(m_updateRect.isNull()){
m_updateRect = dirtyRect;
}
else{
m_updateRect = m_updateRect.united(dirtyRect);
}
update(dirtyRect);
m_lastPoint = pos;
}
}
QString DrawingCanvas::penColor() const
{
return m_penColor;
}
int DrawingCanvas::penWidth() const
{
return m_penWidth;
}
void DrawingCanvas::setDrawing(bool drawing)
{
if (m_drawing == drawing)
return;
m_drawing = drawing;
emit drawingChanged(m_drawing);
}
void DrawingCanvas::setPenWidth(int penWidth)
{
if (m_penWidth == penWidth)
return;
m_penWidth = penWidth;
emit penWidthChanged(m_penWidth);
}
void DrawingCanvas::setPenColor(QString penColor)
{
if (m_penColor == penColor)
return;
m_penColor = penColor;
emit penColorChanged(m_penColor);
}
// initiates the QImage buffer
void DrawingCanvas::initiateBuffer()
{
qDebug() << this << "Initiating buffer" << width() << height();
m_buffer = QPixmap(width(), height());
}
In QML:
import QtQuick 2.12
import QtQuick.Window 2.12
import QtQuick.Controls 2.13
import QtQuick.Layouts 1.12
import QtQuick.Dialogs 1.3
import Drawing 1.0
ApplicationWindow {
visible: true
width: 640
height: 480
title: qsTr("Hello World")
Flickable {
id: scrollView
anchors.fill: parent
contentHeight: drawingCanvas.height
DrawingCanvas {
id: drawingCanvas
width: parent.width
height: 2000
penColor: "red"
onWidthChanged: drawingCanvas.initiateBuffer()
}
}
MouseArea {
anchors.fill: parent
anchors.rightMargin: 20
onPressed: drawingCanvas.penPressed(
Qt.point(mouseX, mouseY + scrollView.contentY))
onPositionChanged: drawingCanvas.penMoved(
Qt.point(mouseX, mouseY + scrollView.contentY))
onReleased: drawingCanvas.penReleased()
}
}
Your rendering issue doesn't seem to be due to the antialising qt option but more to the smoothing of your strokes. I recommend you to modify your custom bezier smoothing techniques or to use a dedicated lib for that [0] .
Secondly, you should create a dedicated QPen in your draw methods and "play" with the QPen and QBrush options [1] if you want the "OneNote drawing feeling". The major difference I saw between the two screenshots was the brush scale dynamics (at the beginning and the end of the strokes).
0: For example, https://github.com/oysteinmyrmo/bezier
1: https://doc.qt.io/qt-5/qpen.html

How can we make a QRubberBand semi-transparent

I have already used
setOpacity();
setAttribute(Qt:WA_TranseculentBackground:)
even i have tied all the available solution nothing has effect.
this is my code
void Physician::mouseMoveEvent(QMouseEvent *e)
{
rubberBand->hide();
bottomRight = e->pos();
QRect rect = QRect(topLeft, bottomRight);
rubberBand->setGeometry(rect);//Area Bounding
QToolTip::showText(e->globalPos(), QString("%1,%2")
.arg(rubberBand->size().width())
.arg(rubberBand->size().height()), this);
}
void Physician::mousePressEvent(QMouseEvent *e)
{
rubberBand->hide();
if(e->x()<ui->videoShowLabel->x()||e->y()<ui->videoShowLabel->y())
{
selectWithInLabel.critical(0,"Error", "Select within the LABEL !");
selectWithInLabel.setFixedSize(500, 200);
}
else{
topLeft = e->pos();
myPoint = ui->videoShowLabel->mapFromGlobal(this->mapToGlobal(e->pos()));
}
}
void Physician::mouseReleaseEvent(QMouseEvent *e){
rubberBand->setWindowOpacity(0.5);
rubberBand->show();
}
void Physician::on_manualROIRadioButton_clicked()
{
rubberBand = new RubberBand(RubberBand::Rectangle, this);
}
What should i do to make rubberBand semiTransparent
I assume you sub classed QRubberBand (RubberBand).
After calling the setWindowopacity the paint event is generated (http://doc.qt.io/qt-5/qwidget.html#windowOpacity-prop)
So redefine the paint event in RubberBand class.
Inside the paint event call "initStyleOption" (given below)
http://doc.qt.io/qt-5/qrubberband.html#initStyleOption
By calling "initStyleOption" you can set the rubber band parameters for drawing.
The real issue with making the QRubberband semi-transparent is that mplayer is painting on a window without Qt having any knowledge of it. Hence Qt itself cannot act as a compositor to generate the required effect.
One possibility would be to make the QRubberBand a top level window. That way the compositing is the responsibility of the underlying graphics system rather than Qt.
With that in mind try the following. Firstly a utility base class to manage the geometry...
class abstract_rubber_band {
public:
virtual QRect get_geometry () const
{
return(QRect(m_parent->mapFromGlobal(widget().geometry().topLeft()), widget().size()));
}
virtual void set_geometry (const QRect &rect)
{
widget().setGeometry(map_rect(rect));
}
protected:
explicit abstract_rubber_band (QWidget *parent)
: m_parent(parent)
{}
/*
* #param point Coords relative to effective parent.
*/
QPoint map_point (QPoint point) const
{
if (point.x() < 0)
point.rx() = 0;
else if (point.x() >= m_parent->width())
point.rx() = m_parent->width() - 1;
if (point.y() < 0)
point.ry() = 0;
else if (point.y() >= m_parent->height())
point.ry() = m_parent->height() - 1;
point = m_parent->mapToGlobal(point);
return(point);
}
QRect map_rect (QRect rect) const
{
return(QRect(map_point(rect.topLeft()), map_point(rect.bottomRight())));
}
private:
QWidget &widget ()
{
return(dynamic_cast<QWidget &>(*this));
}
const QWidget &widget () const
{
return(dynamic_cast<const QWidget &>(*this));
}
QWidget *m_parent;
};
Now use the above as a base of the required rubber band class...
class rubber_band: public abstract_rubber_band,
public QRubberBand {
using super = QRubberBand;
public:
/*
* #param parent Note that this isn't actually used as the
* parent widget but rather the widget to which
* this rubber_band should be confined.
*/
explicit rubber_band (QWidget *parent)
: abstract_rubber_band(parent)
, super(QRubberBand::Rectangle)
{
setAttribute(Qt::WA_TranslucentBackground, true);
}
protected:
virtual void paintEvent (QPaintEvent *event) override
{
QPainter painter(this);
painter.fillRect(rect(), QColor::fromRgbF(0.5, 0.5, 1.0, 0.25));
QPen pen(Qt::green);
pen.setWidth(5);
painter.setPen(pen);
painter.setBrush(Qt::NoBrush);
painter.drawRect(rect().adjusted(0, 0, -1, -1));
/*
* Display the current geometry in the top left corner.
*/
QRect geom(get_geometry());
painter.drawText(rect().adjusted(5, 5, 0, 0),
Qt::AlignLeft | Qt::AlignTop,
QString("%1x%2+%3+%4").arg(geom.width()).arg(geom.height()).arg(geom.left()).arg(geom.top()));
}
};
The above rubber_band class should almost be a drop in replacement for QRubberBand. The main difference is that rather than reading/writing its geometry with geometry/setGeometry you must use get_geometry/set_geometry -- those will perform the mapping to/from global coordinates.
In your particular case create the rubber_band with...
rubberBand = new rubber_band(ui->videoShowLabel);

How to show on QML (Qt) from SQLite BLOB data as image?

My code is like below.
Name - as TEXT field,
Photo - as BLOB data
class SqlQueryModel: public QSqlQueryModel
{
Q_OBJECT
QHash<int,QByteArray> *hash;
public:
explicit SqlQueryModel(QObject * parent) : QSqlQueryModel(parent)
{
hash = new QHash<int,QByteArray>;
hash->insert(Qt::UserRole, QByteArray("Name"));
hash->insert(Qt::UserRole + 1, QByteArray("Photo"));
}
inline RoleNameHash roleNames() const { return *hash; }
};
Selecting data
view = new QQuickView();
QSqlQueryModel *someSqlModel = new SqlQueryModel(this);
someSqlModel->setQuery("SELECT Name, Photo FROM some_table");
QQmlContext *context = view->rootContext();
context->setContextProperty("someSqlModel", someSqlModel);
view->setSource(QUrl("qrc:///MainView.qml"));
view->show();
Binding in QML
ListView {
id: someListView
model: SqlContactModel {}
delegate: ItemDelegate {
text: Name
Image {
id: Photo
source: ???
}
}
}
How to show on QML (Qt) from SQLite BLOB data as image?
You have three options:
let the model hand out some ID and use that with a QQuickImageProvider
let the model hand out a QImage and write a custom item that can display that
let the model hand out the image data as a data URI
For (2) the simplest solution is a QQuickPaintedItem derived class, something like this
class QImageItem : public QQuickPaintedItem
{
Q_OBJECT
Q_PROPERTY(QImage image READ image WRITE setImage NOTIFY imageChanged)
public:
explicit QImageItem(QQuickItem *parent = Q_NULLPTR) : QQuickPaintedItem(parent) {}
QImage image() const { return m_image; }
void setImage(const QImage &image);
void paint(QPainter *painter) Q_DECL_OVERRIDE;
private:
QImage m_image;
};
void QImageItem::setImage(const QImage &image)
{
m_image = image;
emit imageChanged();
update();
setImplicitWidth(m_image.width());
setImplicitHeight(m_image.height());
}
void QImageItem::paint(QPainter *painter)
{
if (m_image.isNull()) return;
painter.drawImage(m_image.scaled(width(), height()));
}
Register as usual with qmlRegisterType<QImageItem>("SomeModuleName", 1, 0, "SomeTypeName") and in QML import SomeModuleName 1.0 and use SomeTypeName in instead of Image, with the QImage returned by the model bound to the item's image property

Issue with drawing an Qml Item with raw OpenGL calls

I want to draw a single item in QtQuick scene using raw OpenGL calls. I have decided to take approach suggested in this question.
I have created a Qt Quick item deriving from QQuickFramebufferObject and exposed it to QML as Renderer: (code is based on Qt example: Scene Graph - Rendering FBOs)
class FboInSGRenderer : public QQuickFramebufferObject {
Q_OBJECT
public:
Renderer *createRenderer() const;
};
source file:
class LogoInFboRenderer : public QQuickFramebufferObject::Renderer {
public:
LogoInFboRenderer() { }
void render() {
int width = 1, height = 1;
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glColor4f(0.0, 1.0, 0.0, 0.8);
glBegin(GL_QUADS);
glVertex2f(0, 0);
glVertex2f(width, 0);
glVertex2f(width, height);
glVertex2f(0, height);
glEnd();
glLineWidth(2.5);
glColor4f(0.0, 0.0, 0.0, 1.0);
glBegin(GL_LINES);
glVertex2f(0, 0);
glVertex2f(width, height);
glVertex2f(width, 0);
glVertex2f(0, height);
glEnd();
update();
}
QOpenGLFramebufferObject *createFramebufferObject(const QSize &size) {
QOpenGLFramebufferObjectFormat format;
format.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil);
format.setSamples(4);
return new QOpenGLFramebufferObject(size, format);
}
};
QQuickFramebufferObject::Renderer *FboInSGRenderer::createRenderer() const {
return new LogoInFboRenderer();
}
In Qml I use it as follows:
import QtQuick 2.4
import SceneGraphRendering 1.0
Rectangle {
width: 400
height: 400
color: "purple"
Renderer {
id: renderer
anchors.fill: parent
}
}
I was expecting to see that rendered "X" will fill entire scene, but instead I get the result presented below:
Other experiments seem to confirm that drew shape has always it's size (width/height) divided by 2.
I also checked that size parameter in createFramebufferObject has correct value.
Looking into docs led me to property textureFollowsItemSize in QQuickFramebufferObject class but it is by default set to true.
Am I doing something wrong or should I consider this behavior as Qt bug?
The drawn rectangle is half the sizes you expect because the default coordinate range is [-1, 1], not [0, 1] as your code assumes. If you want to use [0, 1] scale, then you should appropriately set the projection matrix:
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glOrtho(0.0, 1.0, 0.0, 1.0, -1.0, 1.0);
As Qt documentation says: "Warning: It is crucial that OpenGL operations and interaction with the scene graph happens exclusively on the rendering thread, primarily during the updatePaintNode() call. The best rule of thumb is to only use classes with the "QSG" prefix inside the QQuickItem::updatePaintNode() function." I do it in this way:
*.h
class MyQuickItem : public QQuickItem
{
Q_OBJECT
public:
MyQuickItem();
~MyQuickItem();
protected:
QSGNode *updatePaintNode(QSGNode * oldNode, UpdatePaintNodeData * updatePaintNodeData);
QSGNode *addNode(QSGGeometry *geometry, const QColor &color);
};
*.cpp
MyQuickItem::MyQuickItem()
{
setFlag(QQuickItem::ItemHasContents,true);
}
MyQuickItem::~MyQuickItem()
{
}
QSGNode *MyQuickItem::updatePaintNode(QSGNode *oldNode, QQuickItem::UpdatePaintNodeData *updatePaintNodeData)
{
Q_UNUSED(updatePaintNodeData)
QSGTransformNode *root = static_cast<QSGTransformNode *>(oldNode);
if(!root) root = new QSGTransformNode;
QSGNode *node;
QSGGeometry *geometry;
QSGSimpleRectNode *rect = new QSGSimpleRectNode();
rect->setColor(Qt::green);
rect->setRect(boundingRect());
root->appendChildNode(rect);
geometry = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 2);
geometry->setDrawingMode(GL_LINES);
geometry->setLineWidth(5.0);
geometry->vertexDataAsPoint2D()[0].set(x(), y());
geometry->vertexDataAsPoint2D()[1].set(width(), height());
node = addNode(geometry,Qt::blue);
root->appendChildNode(node);
geometry = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 2);
geometry->setDrawingMode(GL_LINES);
geometry->setLineWidth(5.0);
geometry->vertexDataAsPoint2D()[0].set(width(), y());
geometry->vertexDataAsPoint2D()[1].set(x(), height());
node = addNode(geometry,Qt::blue);
root->appendChildNode(node);
return root;
}
QSGNode *MyQuickItem::addNode(QSGGeometry *geometry, const QColor &color)
{
QSGFlatColorMaterial *material = new QSGFlatColorMaterial;
material->setColor(color);
QSGGeometryNode *node = new QSGGeometryNode;
node->setGeometry(geometry);
node->setFlag(QSGNode::OwnsGeometry);
node->setMaterial(material);
node->setFlag(QSGNode::OwnsMaterial);
return node;
}
In main.cpp
qmlRegisterType<MyQuickItem>("MyObjects", 1, 0, "MyObject");
And usage:
import QtQuick 2.3
import QtQuick.Window 2.2
import MyObjects 1.0
Window {
visible: true
width: 360
height: 360
MyObject {
anchors.fill: parent
}
}

Need QGraphicsScene signal or event for _after_ change

I use QGraphicsScene of the Qt framework. Inside the scene I have some QGraphicsItems which the user can select and move.
I would like to have an info label where the current x and y coordinate of the currently moved selection (can consist of many items) is displayed.
I have tried with the signal changed of QGraphicsScene. But it is fired before the x() and y() property of the items is set to the new values. So the labels always show the second-to-last coordinates. If one moves the mouse slowly, the display is not very wrong. But with fast moves and sudden stops, the labels are wrong. I need a signal that is fired after the scene hast changed.
I have also tried to override the itemChange method of QGraphicsItem. But it is the same. It is fired before the change. (The new coordinates are inside the parameters of this method, but I need the new coordinates of all selected items at once)
I have also tried to override the mouseMove events of QGraphicsScene and of QGraphicsView but they, too, are before the new coordinates are set.
I did a test: I used a oneshot timer so that the labels are updated 100 ms after the signals. Then everything works fine. But a timer is no solution for me.
What can I do?
Make all items un-moveable and handle everything by my own?
QGraphicsItem::itemChange() is the correct approach, you were probably just checking the wrong flag. Something like this should work fine:
QVariant::myGraphicsItem( GraphicsItemChange change, const QVariant &value )
{
if( change == QGraphicsItem::ItemPositionHasChanged )
{
// ...
}
}
Note the use of QGraphicsItem::ItemPositionHasChanged rather than QGraphicsItem::ItemPositionChange, the former is called after the position changes rather than before.
The solution is to combine various things that you're already doing. Instrument itemChange, looking for and count the items with updated geometry. Once you've counted as many items as there are in the current selection, fire off a signal that will have everything ready for updating your status. Make sure you've set the QGraphicsItem::ItemSendsGeometryChanges flag on all your items!
This code was edited to remove the lag inherent in using a zero-timer approach. Below is a sscce that demonstrates it.
You create circles of random radius by clicking in the window. The selection is toggled with Ctrl-click or ⌘-click. When you move the items, a centroid diamond follows the centroid of the selected group. This gives a visual confirmation that the code does indeed work. When the selection is empty, the centroid is not displayed.
I've gratuitously added code to show how to leverage Qt's property system so that the items can be generic and leverage the notifier property of a scene if it has one. In its absence, the items simply don't notify, and that's it.
// https://github.com/KubaO/stackoverflown/tree/master/questions/scenemod-11232425
#include <QtWidgets>
const char kNotifier[] = "notifier";
class Notifier : public QObject
{
Q_OBJECT
int m_count = {};
public:
int count() const { return m_count; }
void inc() { m_count ++; }
void notify() { m_count = {}; emit notification(); }
Q_SIGNAL void notification();
};
typedef QPointer<Notifier> NotifierPointer;
Q_DECLARE_METATYPE(NotifierPointer)
template <typename T> class NotifyingItem : public T
{
protected:
QVariant itemChange(QGraphicsItem::GraphicsItemChange change, const QVariant &value) override {
QVariant v;
if (change == T::ItemPositionHasChanged &&
this->scene() &&
(v=this->scene()->property(kNotifier)).isValid())
{
auto notifier = v.value<NotifierPointer>();
notifier->inc();
if (notifier->count() >= this->scene()->selectedItems().count()) {
notifier->notify();
}
}
return T::itemChange(change, value);
}
};
// Note that all you need to make Circle a notifying item is to derive from
// NotifyingItem<basetype>.
class Circle : public NotifyingItem<QGraphicsEllipseItem>
{
QBrush m_brush;
public:
Circle(const QPointF & c) : m_brush(Qt::lightGray) {
const qreal r = 10.0 + (50.0*qrand())/RAND_MAX;
setRect({-r, -r, 2.0*r, 2.0*r});
setPos(c);
setFlags(QGraphicsItem::ItemIsMovable | QGraphicsItem::ItemIsSelectable |
QGraphicsItem::ItemSendsGeometryChanges);
setPen({Qt::red});
setBrush(m_brush);
}
};
class View : public QGraphicsView
{
Q_OBJECT
QGraphicsScene scene;
QGraphicsSimpleTextItem text;
QGraphicsRectItem centroid{-5, -5, 10, 10};
Notifier notifier;
int deltaCounter = {};
public:
explicit View(QWidget *parent = {});
protected:
Q_SLOT void gotUpdates();
void mousePressEvent(QMouseEvent *event) override;
};
View::View(QWidget *parent) : QGraphicsView(parent)
{
centroid.hide();
centroid.setRotation(45.0);
centroid.setPen({Qt::blue});
centroid.setZValue(2);
scene.addItem(&centroid);
text.setPos(5, 470);
text.setZValue(1);
scene.addItem(&text);
setRenderHint(QPainter::Antialiasing);
setScene(&scene);
setSceneRect(0,0,500,500);
scene.setProperty(kNotifier, QVariant::fromValue(NotifierPointer(&notifier)));
connect(&notifier, &Notifier::notification, this, &View::gotUpdates);
connect(&scene, &QGraphicsScene::selectionChanged, &notifier, &Notifier::notification);
}
void View::gotUpdates()
{
if (scene.selectedItems().isEmpty()) {
centroid.hide();
return;
}
centroid.show();
QPointF centroid;
qreal area = {};
for (auto item : scene.selectedItems()) {
const QRectF r = item->boundingRect();
const qreal a = r.width() * r.height();
centroid += item->pos() * a;
area += a;
}
if (area > 0) centroid /= area;
auto st = QStringLiteral("delta #%1 with %2 items, centroid at %3, %4")
.arg(deltaCounter++).arg(scene.selectedItems().count())
.arg(centroid.x(), 0, 'f', 1).arg(centroid.y(), 0, 'f', 1);
this->centroid.setPos(centroid);
text.setText(st);
}
void View::mousePressEvent(QMouseEvent *event)
{
const auto center = mapToScene(event->pos());
if (! scene.itemAt(center, {})) scene.addItem(new Circle{center});
QGraphicsView::mousePressEvent(event);
}
int main(int argc, char *argv[])
{
QApplication app{argc, argv};
View v;
v.show();
return app.exec();
}
#include "main.moc"

Resources