QGraphicsView and QGraphicsItem: don´t scale item when scaling the view rect - c++

I am using Qt´s QGraphicsView - and QGraphicsItem-subclasses.
is there a way to not scale the graphical representation of the item in the view when the view rectangle is changed, e.g. when zooming in. The default behavior is that my items scale in relation to my view rectangle.
I would like to visualize 2d points which should be represented by a thin rectangle which should not scale when zooming in the view. See a typical 3d modelling software for reference where vertex points are always shown at the same size.
Thanks!

Set the QGraphicItem's flag QGraphicsItem::ItemIgnoresTransformations to true does not work for you?

I got into the same problem, and it took me a while to figure it out. This is how I solved it.
Extend a QGraphicsItem class, override paint().
Inside the paint(), reset the transformation's scaling factor to 1(which are m11 and m22), and save the m11(x scaling factor) and m22(y scaling factor) before the reset.
Then, draw like you would normally do but multiply your x with m11 and y with m22. This avoids drawing with the default transformation, but explicitly calculates the positions according to the scene's transformation.
void MyItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *item, QWidget *widget)
{
QTransform t = painter->transform();
qreal m11 = t.m11(), m22 = t.m22();
painter->save(); // save painter state
painter->setTransform(QTransform(1, t.m12(), t.m13(),
t.m21(), 1, t.m23(), t.m31(),
t.m32(), t.m33()));
int x = 0, y = 0; // item's coordinates
painter->drawText(x*m11, y*m22, "Text"); // the text itself will not be scaled, but when the scene is transformed, this text will still anchor correctly
painter->restore(); // restore painter state
}
The following code block is drawing with default transformation
void MyItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *item, QWidget *widget)
{
int x = 0, y = 0;
painter->drawText(x, y, "Text");
}
You can try both to see the difference. Hope this helps.

How about this:
#include <QtGui/QApplication>
#include <QtGui/QGraphicsScene>
#include <QtGui/QGraphicsView>
#include <QtGui/QGraphicsRectItem>
int main(int argc, char* argv[]) {
QApplication app(argc, argv);
QGraphicsScene scene;
scene.addText("Hello, world!");
QRect rect(50, 50, 100, 100);
QGraphicsRectItem* recti = scene.addRect(rect);
QGraphicsView view(&scene);
// Set scale for the view
view.scale(10.0, 5.0);
// Set the inverse transformation for the item
recti->setTransform(view.transform().inverted());
view.show();
return app.exec();
}
As you can see the text is scaled up but the rectangle is not. Note that this does not only prevent the scaling for the rectangle but and other transformation.

The following solution worked perfectly for me:
void MyDerivedQGraphicsItem::paint(QPainter *painter, const StyleOptionGraphicsItem *option, QWidget *widget)
{
double scaleValue = scale()/painter->transform().m11();
painter->save();
painter->scale(scaleValue, scaleValue);
painter->drawText(...);
painter->restore();
...
}
We can also multiply the scaleValue by other mesures we want to keep its size constant outside the save/restore environment.
QPointF ref(500, 500);
QPointF vector = scaleValue * QPointF(100, 100);
painter->drawLine(ref+vector, ref-vector);

I found that if I derive a new class and reimpliment the paint function I can do
void MyDerivedQGraphicsItem::paint(QPainter *painter,
const QStyleOptionGraphicsItem *option,
QWidget *widget)
{
double scaleValue = scale();
double scaleX = painter->transform().m11();
setScale(scaleValue / scaleX);
QGraphicsSvgItem::paint(painter,option,widget);
}
This is the best way of doing it that I have found so far, but I am still tinkering around.

Related

How to use the QGraphicsItem::setPos() function

I can't figure out how the setPos() function of the QGraphicsItem class works.
My Rect class has no parent, so its origin is relative to the scene.
I try to put the rectangle back at (0, 0) after it is moved with the mouse but it is placed in a different place depending on where I had moved it.
I suppose that means that the origin of the scene moves but what causes this change?
class Rect : public QGraphicsItem {
public:
Rect(): QGraphicsItem()
{
setFlag(ItemIsMovable);
}
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override
{
painter->drawRect(0, 0, 20, 20);
}
void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override
{
setPos(0, 0);
update();
QGraphicsItem::mouseReleaseEvent(event);
}
QRectF boundingRect() const
{
return QRectF(0, 0, 20, 20);
}
private:
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QGraphicsScene scene;
QGraphicsView view(&scene);
Rect obj;
scene.addItem(&obj);
view.show();
return a.exec();
}
When you create a QGraphicsView you initially accept the default settings. A standard setting is, for example, that it is horizontally centered.
Another factor is that the default area size is probably up to the maximum size.
what you can do set a custom size for the scene. You do that with graphicsView->setSceneRect(0,0,300,300); (for example)
scene = new QGraphicsScene(this);
ui->graphicsView->setScene(scene);
ui->graphicsView->setRenderHint(QPainter::Antialiasing);
ui->graphicsView->setSceneRect(0,0, 300,300);
rectItem = new QGraphicsRectItem(0,0, 100, 100);
rectItem->setPen(QPen(Qt::darkMagenta, 2));
rectItem->setBrush(QGradient(QGradient::SaintPetersburg));
rectItem->setPos(190,10);
scene->addItem(rectItem);
So in summary: if you want to work with fixed values. maybe it is better to know the total size. (that was not clear from your code, that's why I gave this example)

How to paint an outline when hovering over a QListWidget item?

I am trying to paint an outline around a QListWidget item when the mouse is over that item. I've subclassed QStyledItemDelegate and overrode paint to account for the QStyle::State_MouseOver case as follows:
class MyDelegate : public QStyledItemDelegate
{
Q_OBJECT
public:
MyDelegate(QObject *parent = nullptr)
: QStyledItemDelegate(parent){}
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override
{
QStyledItemDelegate::paint(painter, option, index);
if(option.state & QStyle::State_MouseOver) painter->drawRect(option.rect);
}
~MyDelegate(){}
};
I then instantiate a QListWidget with some items and enable the Qt::WA_Hover attribute:
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QListWidget w;
w.addItems(QStringList{"item1", "item2", "item3", "item4"});
w.setItemDelegate(new MyDelegate(&w));
w.viewport()->setAttribute(Qt::WA_Hover);
w.show();
return a.exec();
}
Unfortunately, the behaviour is not what I expected. In particular, the outline is painted when I move the mouse over an item, but when I move to another item the outline around the first item is not erased. Instead, it keeps drawing outlines around all the items I move my mouse over and eventually there is an outline around all items. Is this normal? I know that an alternative solution would be to use QStyleSheets but I'd like to understand why the current approach doesn't behave as I expected.
Here is what the widget looks like before a mouse over:
Here it is after hovering over item2:
And then after item3:
I am using Qt 5.15.1 on a MacOS 10.15.6 platform.
EDIT 1:
Based on the answer from scopchanov, to ensure the outline thickness is indeed 1px, I've changed the paint method to this:
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override
{
int outlineWidth = 1;
QPen pen;
pen.setWidth(outlineWidth);
painter->setPen(pen);
QStyledItemDelegate::paint(painter, option, index);
if(option.state & QStyle::State_MouseOver) {
int a = round(0.5*(outlineWidth - 1));
int b = round(-0.5*outlineWidth);
painter->drawRect(option.rect.adjusted(a, a, b, b));
}
}
Unfortunately, the behaviour is very similar; here is a screenshot after hovering over all items from top to bottom:
Cause
QPainter::drawRect draws a rectangle, that is slightly bigger (by exactly one pixel in height and width) than the painted area. The reason for this behavior could be seen in the way QPaintEngine draws a rectangle:
for (int i=0; i<rectCount; ++i) {
QRectF rf = rects[i];
QPointF pts[4] = { QPointF(rf.x(), rf.y()),
QPointF(rf.x() + rf.width(), rf.y()),
QPointF(rf.x() + rf.width(), rf.y() + rf.height()),
QPointF(rf.x(), rf.y() + rf.height()) };
drawPolygon(pts, 4, ConvexMode);
}
QPaintEngine draws a closed polygon, starting at the point (x, y), going to (x + width, y), then to (x + width, y + height) and finally to (x, y + height). This looks intuitive enough, but let's see what happens, if we substitute these variables with real numbers:
Say, we want to draw a 4x2 px rectangle at (0, 0). QPaintEngine would use the following coordinates: (0, 0), (4, 0), (4, 2) and (0, 2). Represented as pixels, the drawing would look like this:
So, instead of 4x2 px, we end up with a 5x3 px rectangle, i.e. indeed one pixel wider and taller.
You can further prove this by clipping the painter to option.rect before calling drawRect like this:
if (option.state & QStyle::State_MouseOver) {
painter->setClipRect(option.rect);
painter->drawRect(option.rect);
}
The result is clipped bottom and right edges of the outline (the very edges, we have expected to be within the painted area):
In any case, the part of the outline, that falls outside of the painted area, is not repainted correctly, hence the unwanted remains of the previous drawings in the form of lines.
Solution
Reduce the height and the width of the outline, using QRect::adjusted.
You might just write
painter->drawRect(option.rect.adjusted(0, 0, -1, -1));
However, this would work only for an outline, which is 1px thick and the devicePixelRatio is 1, as on a PC. If the border of the outline is thicker than 1px and/or the devicePixelRatio is 2, as on a Mac, of course more of the outline will stick out of the painted area, so you should take that into consideration and adjust the rectangle accordingly, e.g.:
int effectiveOutlineWidth = m_outineWidth*m_devicePixelRatio;
int tl = round(0.5*(effectiveOutlineWidth - 1));
int br = round(-0.5*effectiveOutlineWidth);
painter->drawRect(option.rect.adjusted(tl, tl, br, br));
m_outineWidth and m_devicePixelRatio are class members, representing the desired outline width, resp. the ratio between physical pixels and device-independent pixels for the paint device. Provided that you have created public setter methods for them, you could set their values like this:
auto *delegate = new MyDelegate(&w);
delegate->setOutlineWidth(1);
delegate->setDevicePixelRatio(w.devicePixelRatio());
w.setItemDelegate(delegate);
Example
Here is an example I have written for you to demonstrate how the proposed solution could be implemented:
#include <QApplication>
#include <QStyledItemDelegate>
#include <QListWidget>
#include <QPainter>
class MyDelegate : public QStyledItemDelegate
{
int m_outineWidth;
int m_devicePixelRatio;
public:
MyDelegate(QObject *parent = nullptr) :
QStyledItemDelegate(parent),
m_outineWidth(1),
m_devicePixelRatio(1) {
}
void paint(QPainter *painter, const QStyleOptionViewItem &option,
const QModelIndex &index) const override {
QStyledItemDelegate::paint(painter, option, index);
if (option.state & QStyle::State_MouseOver) {
int effectiveOutlineWidth = m_outineWidth*m_devicePixelRatio;
int tl = round(0.5*(effectiveOutlineWidth - 1));
int br = round(-0.5*effectiveOutlineWidth);
painter->setPen(QPen(QBrush(Qt::red), m_outineWidth, Qt::SolidLine,
Qt::SquareCap, Qt::MiterJoin));
painter->drawRect(option.rect.adjusted(tl, tl, br, br));
}
}
void setOutlineWidth(int outineWidth) {
m_outineWidth = outineWidth;
}
void setDevicePixelRatio(int devicePixelRatio) {
m_devicePixelRatio = devicePixelRatio;
}
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QListWidget w;
auto *delegate = new MyDelegate(&w);
delegate->setOutlineWidth(3);
delegate->setDevicePixelRatio(w.devicePixelRatio());
w.setItemDelegate(delegate);
w.addItems(QStringList{"item1", "item2", "item3", "item4"});
w.viewport()->setAttribute(Qt::WA_Hover);
w.show();
return a.exec();
}
Result
The provided example produces the following result for 3px thick outline on Windows:

Qt animate ellipses along the circle

I am new to the Qt and I am confused how to use the QGraphicsScene. If I add for example 10 ellipses to the scene and animate them along the path, how would I do it? It gets even more complicated, because if I have 10 ellipses drawn along the ellipse and I want to animate those ellipses so they move away from the cente of the ellipse they are on. In the picture you can see the ellipses. Those are drawn with QPainter I haven't figured out how to add them to scene, but I would like the grey ellipses to move in between the inner and outter circle. I have gone through some examples, but can't really fit any of those to my situation.
Code used to draw ellipses with QPainter:
QPainter drElps;
drElps.setBrush(QBrush(QColor(0xaf, 0xaf, 0xaa)));
drElps.setPen(QPen(QColor(0xaf, 0xaf, 0xaa), 2));
double nrElps = 30;
int degree = (int)(360./nrElps);
painter.translate(QPointF(big_elps_circle_center));
while(--nrElps){
painter.drawEllipse(0, -60, 3, 3);
painter.rotate(degree);
}
painter.translate(QPOintF(back_to_origin));
Qt has a set of classes oriented to the animations, for this you must first create an object that inherits from QGraphicsObject, in this class you must implement the methods paint and boundingRect.
ellipseobject.h
#ifndef ELLIPSEOBJECT_H
#define ELLIPSEOBJECT_H
#include <QGraphicsObject>
class EllipseObject : public QGraphicsObject
{
Q_OBJECT
public:
EllipseObject(QGraphicsItem * parent = 0);
void paint(QPainter * painter, const QStyleOptionGraphicsItem * option, QWidget * widget = 0);
QRectF boundingRect() const;
};
#endif // ELLIPSEOBJECT_H
ellipseobject.cpp
#include "ellipseobject.h"
#include <QPainter>
EllipseObject::EllipseObject(QGraphicsItem *parent):QGraphicsObject(parent)
{
}
void EllipseObject::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
Q_UNUSED(option)
Q_UNUSED(widget)
painter->setRenderHint(QPainter::Antialiasing);
painter->setPen(QColor(0xaf, 0xaf, 0xaa));
painter->setBrush(QBrush(QColor(0xaf, 0xaf, 0xaa)));
QRectF rect = boundingRect();
painter->drawEllipse(rect);
}
QRectF EllipseObject::boundingRect() const
{
return QRectF(-4, -4, 8, 8);
}
Then we can use the QPropertyAnimation class, with this we will animate the position.
EllipseObject *item = new EllipseObject;
QPropertyAnimation *animation = new QPropertyAnimation(item, "pos");
for(int j = 0; j < p; j++){
animation->setKeyValueAt( 1.0*j/(p-1),
(r+ delta*sin(2*M_PI*j/p) )*QPointF(qSin(2*M_PI*i/number_of_items), qCos(2*M_PI*i/number_of_items)));
}
animation->setDuration(2000);
Since we have several elements that are animated in parallel, we can use QParallelAnimationGroup to handle each of the animations.
group = new QParallelAnimationGroup(this);
[...]
group->addAnimation(animation);
If we want to be continuous we can make the following code.
group->start();
connect(group, &QParallelAnimationGroup::finished,[=](){
group->start();
});
The complete code is here.
If you draw them with QPainter then you are on your on. The point of QGraphivsScene is to inherit from QGraphicsItem, paint what you need to paint on the paintEvent, add them to the scene.
you are drawint more than 1 item on your paintEvent - so you are on your own - and QGraphivsScene cannot help you there.
Now, how to do that Correctly:
Inherit from QGraphicsScene or QGraphivsView
Inherit from QGraphicsEllipseItem and create a MovableEllipseItem
in the MovableEllipseItem constructor, create a QTimer, connect it's timeout() signal with a move slot that you will define
on your move slot, do the calculations on where the ellipse should be and move(x,y) it.
You can use a custom Graphics Item, the QGraphicsScene::advance mechanism, together with a QTimeLine to control the animation.
The outline is like this:
QGraphicsScene *sc = new QGraphicsScene;
QTimeLine *tl = new QTimeLine(1000);
tl->setFrameRange(-20, 20);
connect(tl, &QTimeLine::frameChanged, sc, &QGraphicsScene::advance);
connect(tl, &QTimeLine::finished, tl, &QTimeLine::toggleDirection);
connect(tl, &QTimeLine::finished, tl, &QTimeLine::start);
for (int i = 0; i < 30; ++i) {
sc->addItem(new AnimItem(360./30*i, tl));
}
In the custom AnimItem, you implement the drawing/animation logic. A good base would be QGraphicsEllipseItem. For example:
AnimItem::AnimItem(qreal angle, QTimeLine *timer, QGraphicsItem *parent)
: QGraphicsEllipseItem(0, 0, 3, 3, parent),
m_timer(timer)
{
QTransform t;
t.rotate(angle);
t.translate(0, -120);
setTransform(t);
setBrush(QBrush(QColor(0xaf, 0xaf, 0xaa)));
}
void AnimItem::advance(int phase)
{
if (phase == 1) {
QTransform t = transform();
t.translate(0, m_timer->currentFrame()/5);
setTransform(t);
}
}

How to keep the size and position of QGraphicsItem when scaling the view?

I have some QGraphicsItems in the QGraphicsScene which should keep the same size and position when scaling. I've tried QGraphicsItem::ItemIgnoresTransformations but it turns out that the items get wrong positions. Below is a sample code:
I have subclassed QGraphicsView like this:
class Graphics : public QGraphicsView
{
public:
Graphics();
QGraphicsScene *scene;
QGraphicsRectItem *rect;
QGraphicsRectItem *rect2;
protected:
void wheelEvent(QWheelEvent *event);
};
And in its constructor:
Graphics::Graphics()
{
scene = new QGraphicsScene;
rect = new QGraphicsRectItem(100,100,50,50);
rect2 = new QGraphicsRectItem(-100,-100,50,50);
scene->addLine(0,200,200,0);
rect->setFlag(QGraphicsItem::ItemIgnoresTransformations, true);
scene->addItem(rect);
scene->addItem(rect2);
setScene(scene);
scene->addRect(scene->itemsBoundingRect());
}
The wheelEvent virtual function:
void Graphics::wheelEvent(QWheelEvent *event)
{
if(event->delta() < 0)
scale(1.0/2.0, 1.0/2.0);
else
scale(2, 2);
scene->addRect(scene->itemsBoundingRect());
qDebug() << rect->transform();
qDebug() << rect->boundingRect();
qDebug() << rect2->transform();
qDebug() << rect2->boundingRect();
}
orginal view looks like this:
1
take the line as road and rect aside as a symbol. When zoomed out, the rect maintain its size but jumps out of the scene:
2
which should be that topleft of rect to middle of line. I'm also confused with debug info showing that the boundingRect and transform stays the same, which seems that nothing has changed! What causes the problem and is there any way to solve it? Could someone help? Thank you!
Sorry for delay, now I've solved the problem myself.
I found QGraphicsItem::ItemIgnoresTransformations only works when the point you want stick to is at (0,0) in item's coordinate. You need also update boundingRect manually in this way. Nevertheless, the best solution I've found is subclass QGraphicsItem and set matrix in paint() according to world matrix. Below is my code .
QMatrix stableMatrix(const QMatrix &matrix, const QPointF &p)
{
QMatrix newMatrix = matrix;
qreal scaleX, scaleY;
scaleX = newMatrix.m11();
scaleY = newMatrix.m22();
newMatrix.scale(1.0/scaleX, 1.0/scaleY);
qreal offsetX, offsetY;
offsetX = p.x()*(scaleX-1.0);
offsetY = p.y()*(scaleY-1.0);
newMatrix.translate(offsetX, offsetY);
return newMatrix;
}
And the paint function:
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option,
QWidget *widget)
{
QPointF p(left, top);
painter->setMatrix(stableMatrix(painter->worldMatrix(), p));
painter->drawRect(left, top, width, height);
}
The second argument of stableMatrix is sticked point, in my sample code it's top-left of the item. You can change it to your preference. It works really fine!
Hope this post help :)
The solution to this is even simpler.
QGraphicsItem::ItemIgnoresTransformations
The item ignores inherited transformations (i.e., its position is still anchored to its parent, but the parent or view rotation, zoom or shear transformations are ignored). [...]
And that's the key! Item ignores all transformations, but is still bound to its parent. So you need two items: a parent item that will keep the relative position (without any flags set) and a child item that will do the drawing (with QGraphicsItem::ItemIgnoresTransformations flag set) at parent's (0,0) point.
Here is some working code of a crosshair that have constant size and rotation, while keeping the relative position to its parent:
#include <QGraphicsItem>
#include <QPainter>
class CrossHair : public QGraphicsItem
{
private:
class CrossHairImpl : public QGraphicsItem
{
public:
CrossHairImpl (qreal len, QGraphicsItem *parent = nullptr)
: QGraphicsItem(parent), m_len(len)
{
setFlag(QGraphicsItem::ItemIgnoresTransformations);
}
QRectF boundingRect (void) const override
{
return QRectF(-m_len, -m_len, m_len*2, m_len*2);
}
void paint (QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) override
{
painter->setPen(QPen(Qt::red, 1));
painter->drawLine(0, -m_len, 0, m_len);
painter->drawLine(-m_len, 0, m_len, 0);
}
private:
qreal m_len;
};
public:
CrossHair (qreal x, qreal y, qreal len, QGraphicsItem *parent = nullptr)
: QGraphicsItem(parent), m_impl(len, this) // <-- IMPORTANT!!!
{
setPos(x, y);
}
QRectF boundingRect (void) const override
{
return QRectF();
}
void paint (QPainter *, const QStyleOptionGraphicsItem *, QWidget *) override
{
// empty
}
private:
CrossHairImpl m_impl;
};

QPainter::drawImage() clips QImage when X, Y are not 0 on a QDeclarativeItem

I'm developing a new QML element in C++ based on this sample code. My class inherits from QDeclarativeItem and it's paint() method is responsible to draw a QImage to the screen:
void NewQMLitem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
// image is QImage* that was loaded at the constructor of the class
painter->drawImage(0, 0, *image);
}
The widget size is 800x480 and the image is 400x240. The code below works perfectly as long as the drawing starts at (0, 0), as you can see below:
The problem I'm facing is that drawing at an arbitrary coordinate such as(200, 0), seems to clip the drawing. It looks like QPainter only updates the screen starting from (0, 0):
void NewQMLitem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
painter->drawImage(200, 0, *image);
}
And the following is the result:
My current workaround involves calling widget->update() at the end of the paint(). Of course, this is terrible because it makes the widget be painted twice.
What is the proper way to handle this? I'm currently using Qt 5.2 on Windows.
I found out that in these situations you need to call prepareGeometryChange() to let the parent know that the size/position of your widget has changed, and that it needs to query the new geometry to be able to paint it correctly to the screen.
This will make the parent invoke boundingRect() on your widget, so it's your responsibility to implement it:
void NewQMLitem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
prepareGeometryChange()
painter->drawImage(200, 0, *image);
}
QRectF NewQMLitem::boundingRect() const
{
return QRectF(m_x, m_y, m_width, m_height);
}