I have QGraphicsItems on a QGraphicsScene. I need to group them. The main reason for grouping: I need to provide a way to apply transformations on a group.
It is all very simple if I need to apply a single transformation, and the items bounding rectangle top-left is at 0,0.
I have seen how grouping and ungrouping works, preserving items' transformations relative to the scene, and that is great. But on grouping, no matter what positions my items have, the group has coordinates (0,0).
When I try any transformations, no matter what I tried with the transformation origin point, the item moves.
Simple program to show:
#include <QApplication>
#include <QGraphicsView>
#include <QGraphicsRectItem>
#include <QDebug>
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QGraphicsScene s;
QGraphicsView view(&s);
s.setSceneRect(90, -10, 420, 140);
view.show();
QGraphicsRectItem* sceneR = new QGraphicsRectItem(s.sceneRect());
s.addItem(sceneR);
QTransform t; // simple scaling transform
t.scale(2, 1); // adding rotation scares me
QGraphicsRectItem* r0 = new QGraphicsRectItem(QRectF(0, 0, 150, 100)); // for compare, not scaled
r0->setBrush(QBrush(Qt::red));
r0->setPos(100, 0);
s.addItem(r0);
QGraphicsRectItem* r1 = new QGraphicsRectItem(QRectF(0, 0, 150, 100)); // for compare, not scaled
r1->setBrush(QBrush(Qt::blue));
r1->setPos(100, 10);
s.addItem(r1);
QGraphicsRectItem* r2 = new QGraphicsRectItem(QRectF(0, 0, 150, 100)); // will make a group of a single item
r2->setFlags(QGraphicsItem::ItemIsSelectable);
r2->setBrush(QBrush(Qt::green));
r2->setPos(100, 20);
r2->setSelected(true);
s.addItem(r2);
QGraphicsItemGroup* g = s.createItemGroup(s.selectedItems());
// QPointF topLeft = g->mapToScene(g->boundingRect().topLeft());
// g->setTransformOriginPoint(topLeft);
r1->setTransform(t);
g->setTransform(t);
qDebug() << r1->pos() << r1->transform();
qDebug() << g->pos() << g->transform();
return app.exec();
}
If I apply transformations, the grouped item moves. I have not been able to figure out a logic or how to compensate.
(red = reference, not scaled; blue, reference, scaled; green, grouped and scaled)
In image above I expect the green rectangle to have the same x coordinate as the blue (I shifted vertically to see what is going on)
How can I make my scaling (and rotation) in place ?
(I am considering the alternative of just setting a common parent, and performing all the work to retrieve items transformations, but not sure how to combine them properly, especially scaling a group how to find the new position and new scale of inside items)
Found a solution - even though I am counting on the group to perform item transformations and calculate its own edges, I must do it manually as well.
Steps:
I find the top-left most point (minX, minY) of selected items' bounding rectangles (mapped to scene),
then move all items by -minX, -minY,
then after grouping, move group by minX, minY.
This sets a correct center of transformations, so the group item behaves like regular items. It also reports a pos() in its top left corner, similar to the default pos() of other QGraphicsItems.
Related
I am creating some images rendering the contents of a QGraphicsScene.
My project requirement is that it should handle a canvas size of 10 ft by 8 inches. On screen, and scene size, that is 8640 x 576 pixels.
I can render it fine.
The thing is, the output images need to have 300 resolution.
That means, the rendered image will have a width of 36000, which is over 2^15 - 1 = 32767 pixels.....
The output is clipped - in the code below, I would get a QImage of correct expected size (36000) but the QGraphicsScene only renders to 32767 pixels.
That is confusing... I cannot explain the outcome - if the QImage limitations were 32767 pixels, then I should not be able to create one in the first place. But I checked and the QImage "sanity check" is much higher.
Once the image is created, I do not see anything in the code for rendering QGraphicsScene that would clip at any value....
This is a simple code that is trying to expose my problem.
It creates a QImage of required size, and fills with yellow (for control).
Then it renders a QGraphicsScene with blue background brush and a red rectangle close to the right margin.
If it works correctly, the result should be: an image of width 36000, blue with a tiny red rectangle at the far right.
But... as it is, the result is an image of width 36000, blue for the first 32766 pixels then yellow for the rest, no red rectangle.
#include <QApplication>
#include <QGraphicsView>
#include <QGraphicsRectItem>
#include <QPainter>
void printScene(QGraphicsScene* s, qreal ratio) {
qreal w = s->width() * ratio;
qreal h = s->height() * ratio;
QRectF target(0, 0, w, h);
QImage image = QImage(w, h, QImage::Format_ARGB32_Premultiplied);
image.fill(QColor(Qt::yellow).rgb());
QPainter painter;
painter.begin(&image);
s->render(&painter, target);
painter.end();
image.save("image.png");
}
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
QGraphicsScene s;
s.setSceneRect(0, 0, 8640, 576);
s.setBackgroundBrush(Qt::blue);
QGraphicsView view(&s);
view.show();
QGraphicsRectItem* r = s.addRect(8530, 250, 100, 100);
r->setBrush(Qt::red);
qreal ratio = 300/72.;
printScene(&s, ratio);
return app.exec();
}
As seen in sample images, the QImage is created successfully, QGraphicsScene though only renders to 2^15 - 1... But I stepped through their code and I didn't see it stop....
(I also tried creating the original scene 36000 x something (and setting the ratio to 1), and it displays fine... it just won't render to QImage anything beyond 32767 pixels)
Am I missing some setting ? What could be the cause of the QGraphicsScene::render() to not render more ?
I would love to find out how I can render the size I want - width of 36000 pixels - or a reason why this is not possible.
I am running this in Windows 7, 32 bit Qt 5.5.1 or 4.7.4
I have found the reason for the clipping - and imagined 2 workarounds.
Why:
Stepping through the rendering code, the clip rect gets limited to 32767:
bool QRasterPaintEngine::setClipRectInDeviceCoords(const QRect &r, Qt::ClipOperation op)
{
Q_D(QRasterPaintEngine);
QRect clipRect = r & d->deviceRect;
...
}
Where deviceRect is set by
void QRasterPaintEnginePrivate::systemStateChanged()
{
deviceRectUnclipped = QRect(0, 0,
qMin(QT_RASTER_COORD_LIMIT, device->width()),
qMin(QT_RASTER_COORD_LIMIT, device->height()));
QRegion clippedDeviceRgn = systemClip & deviceRectUnclipped;
deviceRect = clippedDeviceRgn.boundingRect();
baseClip->setClipRegion(clippedDeviceRgn);
...
}
and
// This limitations comes from qgrayraster.c. Any higher and
// rasterization of shapes will produce incorrect results.
const int QT_RASTER_COORD_LIMIT = 32767;
Options:
1) Render to a max of 32767 and, if the target must be bigger, scale result. (should give slightly lower quality)
2) Create 2 images and combine them (I still need to figure that out but I think it is the better fix)
I can't understand how scaling and rotation are applied to QGraphicsItem.
I need to be able to apply rotation and scaling (not necessarily keeping aspect ratio) and I get fully unexpected results.
Rotation must be around the item center. I seem to have no problem doing that - yet if I try to debug the bounding rectangle, I get seemingly wrong values.
If I don't keep aspect ratio, instead of rotation I get a very weird skew, and I have been struggling for quite a while to find the cause and correct it. I hope anybody can find a solution.
For many items - like rectangles - my solution was to give up on resize - and just replace the item with a new one of given size. I even did that for pixmap (though it probably will affect performance a lot).
But I don't know how to do that for text, or a few other types (svg...).
I am trying to understand how scaling is applied, on rotated items, and how to get it applied correctly.
The code below is an experiment where I scale and rotate a text item, and the result... see attached image
#include <QApplication>
#include <QGraphicsView>
#include <QGraphicsTextItem>
void experimentScaling(QGraphicsScene* s)
{
QGraphicsTextItem* ref = new QGraphicsTextItem(); // a reference, not resized
ref->setPlainText("hello world");
s->addItem(ref);
ref->setDefaultTextColor(Qt::red);
ref->setRotation(45);
QGraphicsTextItem* t = new QGraphicsTextItem(); // text item to be experimented on
t->setPlainText("hello world");
s->addItem(t);
QTransform transform; // scale
transform.scale(10, 1);
t->setTransform(transform);
t->update();
QPointF _center = t->boundingRect().center();
qDebug("%f %f %f %f", t->boundingRect().left(), t->boundingRect().top(), t->boundingRect().right(), t->boundingRect().bottom()); // seems to be unscaled...
t->setTransformOriginPoint(_center); // rotation must be around item center - and seems to work even though the bounding rect gives wrong values above
t->setRotation(45); // skewed
t->update();
}
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QGraphicsScene s;
QGraphicsView view(&s);
s.setSceneRect(-20, -20, 800, 600);
view.show();
experimentScaling(&s);
return app.exec();
}
Reference (red) text rotated 45 degrees, text rotated 45 degrees and resized 10,1:
The resized (black) text should have the same height as the reference (red) - yet is much taller;
The bounding rectangle is no longer a rectangle - it is skewed;
The angle looks much smaller than 45;
Added a resized but not rotated reference as well:
Please help me understand why this behavior is happening and what can I do about it.
I have tried looking into QGraphicsRotation but I can't figure out how to apply it... All I get is a move instead of rotation.
As documented, the item's transformations are mathematically applied in a certain order - this is the order you'd be multiplying the transform matrices in and is, conceptually, the reverse of the order you'd normally think of.
The transform is applied. The origin point must be included in the transform itself, by applying translations during the transform.
The transformations are applied - each of them can specify its own center.
rotation then scale are applied, both relative to transformOriginPoint.
When you set transform to scaling, and set rotation, the rotation is performed before scaling. The scaling applies to the rotated result - it simply stretches the rotated version horizontally in your case.
You need to somehow enforce the reverse order of operations. The only two ways to do that are:
Stack the transforms in correct order and pass them to transform, or.
Pass a list of correct transformations to transformations.
I'll demonstrate how to do it either way, in an interactive fashion where you can adjust the transform parameters using sliders.
To obtain the correct result using transform:
QGraphicsItem * item = ....;
QTransform t;
QPointF xlate = item->boundingRect().center();
t.translate(xlate.x(), xlate.y());
t.rotate(angle);
t.scale(xScale, yScale);
t.translate(-xlate.x(), -xlate.y());
item->setTransform(t);
To obtain the correct result using transformations:
QGraphicsItem * item = ....;
QGraphicsRotation rot;
QGraphicsScale scale;
auto center = item->boundingRect().center();
rot.setOrigin(QVector3D(center));
scale.setOrigin(QVector3D(center()));
item->setTransformations(QList<QGraphicsTransform*>() << &rot << &scale);
Finally, the example:
// https://github.com/KubaO/stackoverflown/tree/master/questions/graphics-transform-32186798
#include <QtWidgets>
struct Controller {
public:
QSlider angle, xScale, yScale;
Controller(QGridLayout & grid, int col) {
angle.setRange(-180, 180);
xScale.setRange(1, 10);
yScale.setRange(1, 10);
grid.addWidget(&angle, 0, col + 0);
grid.addWidget(&xScale, 0, col + 1);
grid.addWidget(&yScale, 0, col + 2);
}
template <typename F> void connect(F && f) { connect(f, f, std::forward<F>(f)); }
template <typename Fa, typename Fx, typename Fy> void connect(Fa && a, Fx && x, Fy && y) {
QObject::connect(&angle, &QSlider::valueChanged, std::forward<Fa>(a));
QObject::connect(&xScale, &QSlider::valueChanged, std::forward<Fx>(x));
QObject::connect(&yScale, &QSlider::valueChanged, std::forward<Fy>(y));
}
QTransform xform(QPointF xlate) {
QTransform t;
t.translate(xlate.x(), xlate.y());
t.rotate(angle.value());
t.scale(xScale.value(), yScale.value());
t.translate(-xlate.x(), -xlate.y());
return t;
}
};
int main(int argc, char **argv)
{
auto text = QStringLiteral("Hello, World!");
QApplication app(argc, argv);
QGraphicsScene scene;
QWidget w;
QGridLayout layout(&w);
QGraphicsView view(&scene);
Controller left(layout, 0), right(layout, 4);
layout.addWidget(&view, 0, 3);
auto ref = new QGraphicsTextItem(text); // a reference, not resized
ref->setDefaultTextColor(Qt::red);
ref->setTransformOriginPoint(ref->boundingRect().center());
ref->setRotation(45);
scene.addItem(ref);
auto leftItem = new QGraphicsTextItem(text); // controlled from the left
leftItem->setDefaultTextColor(Qt::green);
scene.addItem(leftItem);
auto rightItem = new QGraphicsTextItem(text); // controlled from the right
rightItem->setDefaultTextColor(Qt::blue);
scene.addItem(rightItem);
QGraphicsRotation rot;
QGraphicsScale scale;
rightItem->setTransformations(QList<QGraphicsTransform*>() << &rot << &scale);
rot.setOrigin(QVector3D(rightItem->boundingRect().center()));
scale.setOrigin(QVector3D(rightItem->boundingRect().center()));
left.connect([leftItem, &left]{ leftItem->setTransform(left.xform(leftItem->boundingRect().center()));});
right.connect([&rot](int a){ rot.setAngle(a); },
[&scale](int s){ scale.setXScale(s); }, [&scale](int s){ scale.setYScale(s); });
right.angle.setValue(45);
right.xScale.setValue(3);
right.yScale.setValue(1);
view.ensureVisible(scene.sceneRect());
w.show();
return app.exec();
}
I was able to make that work by using two separate QTransforms and multiplying them together. Note that the order of transformations matter:
QTransform transform1;
transform1.scale(10, 1);
QTransform transform2;
transform2.rotate(45);
t->setTransform(transform1 * transform2);
I am drawing a line using mouse clicks. The line is drawn using paint function as:
painter->drawLine(start_p, end_p);
The bounding rect of line is defined as:
QRectF Line::boundingRect() const
{
// bounding rectangle for line
return QRectF(start_p, end_p).normalized();
}
This shows the line painted. I get the bounding rect for this as shown:
I want to have the bounding rect according to the shape of the item, something like:
How to achieve this?
Edit
While selecting any of the overlapping lines, the one with bounding rect on top is selected(see figure below). Even making use of setZValue won't work here.
I want to implement this by minimizing the bounding rect to the shape of line.
If you have an item that is not shaped like a rectangle, or is a rotated rectangle use QGraphicsItem::shape.
This function should return a QPainterPath. You should be able to create your path by using QPainterPath::addPolygon.
Here is a small example:
QPainterPath Item::shape() const
{
QPainterPath path;
QPolygon polygon;
polygon << QPoint(0, 0);
polygon << QPoint(5, 5);
polygon << QPoint(width, height);
polygon << QPoint(width - 5, height - 5);
path.addPolygon(polygon);
return path;
}
You of course should calculate your points inside the path in a different way, but you get the point. Now when you click on an item, it will only select it if the click happened inside the shape defined by the QPainterPath.
If you ever need to make curvy lines, you can use QPainterPathStroker::createStroke as suggested by cmannett85.
There are two relevant functions in a QGraphicsItem that you should be interested in. The first is boundingRect. This, as you probably realise is a rectangle which encompasses the whole item. Qt uses this for such things as quickly calculating how much of an item is visible and simple item collision.
That's great if you have rectangular items; you can just override boundingRect() in any items you inherit from QGraphicsItem or QGraphicsObject.
If you have a shape that isn't regular and you want to do things such as collision with an item's shape, then theshape() function needs overriding too in your class.
This returns a QPainterPath, so you can do something like this: -
QPainterPath Line::shape()
{
QRectF rect(start_p, end_p).normalized();
// increase the rect beyond the width of the line
rect.adjust(-2, -2, 2, 2);
QPainterPath path;
path.addRect(rect);
return path; // return the item's defined shape
}
Now, you can use a painter to draw the shape() item, instead of the boundingRect() and collision will work as expected.
boundingRect is always used for optimize painting process of of scene. So you have have no room for manipulation here.
BUT if you want change area for mouse interaction there is shape method. By default this method returns QPainterPath rectangle received from boundingRect method.
So just override this method and provide desired shape.
QPainterPath YourGraphicsItem::shape() const {
static const qreal kClickTolerance = 10;
QPointF vec = end_p-start_p;
vec = vec*(kClickTolerance/qSqrt(QPointF::dotProduct(vec, vec)));
QPointF orthogonal(vec.y(), -vec.x());
QPainterPath result(start_p-vec+orthogonal);
result.lineTo(start_p-vec-orthogonal);
result.lineTo(end_p+vec-orthogonal);
result.lineTo(end_p+vec+orthogonal);
result.closeSubpath();
return result;
}
You must draw yourself bounding if you want some thing like this. let Qt have it's QRect for bounding and define your new QRect dependent to the corner of previous QRect, top-left and bottom-right. for example if the top-left corner is (2,2) your new QRect top-left is (1,2) and top-right is (2,1) and ....
I want to draw 1 digit on the screen by the graphic framework classes. I want the fill approach of '1' to be something like
(source: qt-project.org)
but the brush of my drawn '1' is just like a yellow SolidBrush by the below code (an ugly bold yellow '1'). Can you help me what's wrong with it?
QGraphicsSimpleTextItem digit_1 = new QGraphicsSimpleTextItem;
digit_1->setText(QString::number(1));
digit_1->setPen(QPen(QColor("black")));
QLinearGradient gradient(digit_1->boundingRect().topLeft(),
digit_1->boundingRect().bottomRight());
gradient.setColorAt(0, Qt::white);
gradient.setColorAt(1, Qt::yellow); // yellow is for example
QBrush brush(gradient);
brush.setStyle(Qt::BrushStyle::LinearGradientPattern);
digit_1->setBrush(brush);
digit_1->setFont(QFont("courier", 35, QFont::Black));
Thanks in advanced.
Your issue most likely comes from the fact that you're basing your gradient's "area" on the bounding rect of your item before you set the font size to something much larger than the default.
The bounding rect you're getting is thus much smaller than your actual bounding rect. Since the default spread method is padding, you're seeing most likely just one color (or not enough of the gradient for it to be actually visible).
So move your setFont call to the top, before you create the gradient. You can drop the setStyle on your brush, that's determined automatically from the gradient. (In fact, you can drop that brush entirely and use the gradient in setBrush.)
With the way you set up the gradient, you'll get a "diagonal" gradient. If you want it from top to bottom, use the top left and bottom left points instead.
Demo
#include <QtGui>
class W: public QGraphicsView
{
Q_OBJECT
public:
W(QWidget *parent = 0)
: QGraphicsView(parent)
{
QGraphicsSimpleTextItem *item = new QGraphicsSimpleTextItem;
item->setText("Stack Overflow");
item->setPen(QPen(Qt::red));
item->setFont(QFont("courier", 60, QFont::Bold));
QLinearGradient lgrad(item->boundingRect().topLeft(),
item->boundingRect().bottomLeft());
lgrad.setColorAt(0.0, Qt::red);
lgrad.setColorAt(1.0, Qt::yellow);
item->setBrush(lgrad);
QGraphicsScene *scene = new QGraphicsScene;
scene->setBackgroundBrush(QBrush(Qt::black));
scene->addItem(item);
setScene(scene);
}
};
One amazing feature of QGraphicsView is its ability to scale itself with its scene's content (every QGraphicsItem inserted in the scene actually). The QPixmap that I have inserted scales correctly, meaning that if I provide a scale factor of 4x4 with this:
view->scale(4,4);
Pixmap are zoomed as I want to do.
But this is not the case of the rects that I am used to drawing; they aims to surrounds the pixmaps that I draw on my scene and regardless of the scale factor, they keep a thickness of 1 instead of - I guess - 4.
I have been searching documentation about all of that stuff, trying to figure out the exact purpose of "cosmetics pen", but I still can't manage to make my rectangle go thicker.
Last notice: I have a custom QGraphicsItem and the QPen which is used to draw the rectangled is instanciated on-the-fly in the
virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget);
method.
Does it matter?
Thanks in advance and apologies for my lack of experience / knowledge in both the Qt framework and the drawing algorithms fields...
It doesn't really matter where you instantiate the QPen.
QPen has a default width of 0. This is a special value that means cosmetic is true and the width is actually 1. So if you don't want the pen to be cosmetic you have to set it to the desired width. You might also need to set cosmetic to false.
Here is a simple example:
#include <QtGui>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QGraphicsView view;
QGraphicsScene scene;
QGraphicsRectItem *item1 = scene.addRect(20, 20, 20, 20);
QGraphicsRectItem *item2 = scene.addRect(50, 20, 20, 20);
QPen pen1, pen2;
pen1.setWidth(5);
pen2.setWidth(5);
pen1.setCosmetic(true);
pen2.setCosmetic(false);
item1->setPen(pen1);
item2->setPen(pen2);
view.setScene(&scene);
view.scale(4, 4); // both rects are the same size, but one a has wider pen
view.show();
return a.exec();
}