How to rescale GraphicItems without disturbing the shape?
I am trying to rescale QGraphicsItems from a part of a scene. When I rescale the QGraphicsItem it transforms the shape.
QList<QGraphicsItem*> items;
for (auto it : items)
{
qreal scale = 1.75;
QPointF c = it->mapToScene(it->boundingRect().center());
it->setScale(scale);
QPointF cNew = it->mapToScene((it->boundingRect()).center());
QPointF offset = c - cNew;
it->moveBy(offset.x(), offset.y());
}
Create QGraphicsItemGroup which is containing all your items and apply the transform on it.
You just have to recenter the group according to the previous position.
QList<QGraphicsItem*> makeItems()
{
return QList<QGraphicsItem*>() << new QGraphicsLineItem(0, 0, 50, 0)
<< new QGraphicsLineItem(50, 0, 50, 50)
<< new QGraphicsLineItem(50, 50, 0, 50)
<< new QGraphicsLineItem(0, 50, 0, 0)
<< new QGraphicsRectItem(24, 24, 2, 2)
<< new QGraphicsEllipseItem(0, 0, 50, 50);
}
int main(int argc, char *argv[])
{
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QApplication app(argc, argv);
QGraphicsScene* scene = new QGraphicsScene(-500, -500, 1000, 1000);
auto* view = new QGraphicsView;
view->setScene(scene);
QList<QGraphicsItem*> items, originalItems;
originalItems = makeItems();
items = makeItems();
QGraphicsItemGroup* itemGroup = new QGraphicsItemGroup;
for (auto* item: originalItems)
{
scene->addItem(item);
}
for (auto* item: items)
{
itemGroup->addToGroup(item);
}
scene->addItem(itemGroup);
qreal scale = 1.75;
QPointF const beforeScale = itemGroup->mapToScene(itemGroup->boundingRect().center());
itemGroup->setScale(scale);
QPointF const afterScale = itemGroup->mapToScene(itemGroup->boundingRect().center());
QPointF const offset = beforeScale - afterScale;
itemGroup->moveBy(offset.x(), offset.y()); // The center will be the same than before the scaling
view->show();
return app.exec();
}
Use QPen::setCosmetic to keep the look-and-feel before and after the transformation
Related
Consider this class called Hex to draw a hexagon :
// Hex.h
#include <QGraphicsPolygonItem>
#include <QPointF>
#include <QVector>
#include <QImage>
#include <QBrush>
#include <QPen>
class Hex : public QGraphicsPolygonItem {
public:
// constructor
Hex(QGraphicsItem* parent = NULL);
// getters/setters
QPolygonF *getHexagon() {return hexagon;}
QBrush *getBrush() {return brush;}
protected:
QBrush *brush;
QPolygonF *hexagon;
};
and
// Hex.cpp
#include "hex.h"
Hex::Hex(QGraphicsItem *parent) {
// set Points
QVector<QPointF> hexPoints;
hexPoints << QPointF(1, 0) << QPointF(0, 1) << QPointF(0, 2)
<< QPointF(1, 3) << QPointF(2, 2) << QPointF(2 ,1);
// scale the poly
int SCALE_BY = 40;
for (size_t i = 0, n = hexPoints.size(); i < n ; i++) {
hexPoints[i] *= SCALE_BY;
}
//create a QPyolygon with the scaled points
hexagon = new QPolygonF(hexPoints);
// draw the polygon
setPolygon(*hexagon);
}
In main.cpp :
#include <QGraphicsView>
#include <QGraphicsScene>
#include "hex.h"
int main(int argc, char *argv[]){
QApplication a(argc, argv);
QGraphicsScene *scene = new QGraphicsScene();
QGraphicsView *view = new QGraphicsView();
Hex *hex0 = new Hex();
Hex *hex1 = new Hex();
hex0->setPos(400, 400);
hex1->setPos(300, 300);
scene->addItem(hex0);
scene->addItem(hex1);
scene->setSceneRect(0,0 , 1024, 768);
view->setScene(scene);
view->show();
return a.exec();
}
Ok. this is ok and setPos works fine.
But using addPolygon instead of addItem to set picture for the hexagons results in setPos not working and all the hexagons cover each other at the first coordinates given to them.
Like this :
// main.cpp
#include <QApplication>
#include <QGraphicsView>
#include <QGraphicsScene>
#include "hex.h"
int main(int argc, char *argv[]){
QApplication a(argc, argv);
QGraphicsScene *scene = new QGraphicsScene();
QGraphicsView *view = new QGraphicsView();
Hex *hex0 = new Hex();
Hex *hex1 = new Hex();
hex0->setPos(400, 400);
hex1->setPos(300, 300);
// set picture for hexagons:
QBrush* brush = new QBrush(QImage("image.png"));
scene->addPolygon(*hex0->getHexagon(), QPen(Qt::black) ,*brush);
scene->addPolygon(*hex1->getHexagon(), QPen(Qt::black) ,*brush);
scene->setSceneRect(0,0 , 1024, 768);
view->setScene(scene);
view->show();
return a.exec();
}
Why does setPos() has this behavior and how to move the pictured hexagons.
Thanks in advance.
setPos () does not modify the QPolygonF but moves the item in scene coordinates, instead QPolygonF are drawn with respect to the internal coordinate system of the item. Therefore, if you want to observe the initial behavior, you have 2 options:
Move the QGraphicsPolygonItem:
QGraphicsPolygonItem *p0 = scene->addPolygon(*hex0->getHexagon(), QPen(Qt::black) ,*brush);
QGraphicsPolygonItem *p1 = scene->addPolygon(*hex1->getHexagon(), QPen(Qt::black) ,*brush);
p0->setPos(400, 400);
p1->setPos(300, 300);
Move the QPolygonF:
scene.addPolygon((*hex0->getHexagon()).translated(400, 400), QPen(Qt::black));
scene.addPolygon((*hex0->getHexagon()).translated(300, 300), QPen(Qt::black));
Your confusion comes from the fact that QGraphicsPolygonItem already has a QPolygonF and a QBrush member, you don't need extra ones.
Not only that, but addPolygon constructs a whole new QGraphicsPolygonItem:
Creates and adds a polygon item to the scene, and returns the item pointer. The polygon is defined by polygon, and its pen and brush are initialized to pen and brush.
I would suggest that Hex shouldn't be a class, it should be a function:
QGraphicsSceneItem * makeHex(QPointF pos, int scale = 40) {
QVector<QPointF> hexPoints{ QPointF(1, 0), QPointF(0, 1), QPointF(0, 2), QPointF(1, 3), QPointF(2, 2), QPointF(2 ,1) };
// scale the poly
for (auto & point : hexpoints) {
point *= scale;
}
auto * hex = new QGraphicsPolygonItem();
hex->setPolygon(hexPoints);
hex->setPos(pos);
hex->setBrush(QImage("image.png"));
return hex;
}
Or
QGraphicsSceneItem * addHex(QGraphicsScene *scene, int scale = 40) {
QVector<QPointF> hexPoints{ QPointF(1, 0), QPointF(0, 1), QPointF(0, 2), QPointF(1, 3), QPointF(2, 2), QPointF(2 ,1) };
// scale the poly
for (auto & point : hexpoints) {
point *= scale;
}
return scene->addPolygon(hexpoints, QPen(Qt::black), QImage("image.png"));
}
Following my previous post I have been trying to solve a positioning problem withing a QGraphicsView.
After a widget is dragged from a QListWidget and dropped inside a QGraphicsView, it jumps everywhere.
The problem I have been trying to solve with also the help of another user is the following:
As soon as the widget is dragged inside the QGraphicsView and dropped. It just goes in random locations and have to pay attention to retrieve it. This behavior is not user friendly and sometimes it takes a bit to locate the widget.
Below the code:
scene.h
#ifndef SCENE_H
#define SCENE_H
#include <QGraphicsScene>
class Scene : public QGraphicsScene
{
public:
Scene(QObject *parent = nullptr);
protected:
void dragEnterEvent(QGraphicsSceneDragDropEvent *event);
void dragMoveEvent(QGraphicsSceneDragDropEvent *event);
void dropEvent(QGraphicsSceneDragDropEvent *event);
};
#endif // SCENE_H
scene.cpp
void Scene::dropEvent(QGraphicsSceneDragDropEvent *event)
{
QByteArray encoded =
event->mimeData()->data("application/x-qabstractitemmodeldatalist");
QDataStream stream(&encoded, QIODevice::ReadOnly);
QStringList rosTables;
QString newString;
while (!stream.atEnd()) {
int row, col;
QMap<int, QVariant> roleDataMap;
stream >> row >> col >> roleDataMap;
rosTables << roleDataMap[Qt::DisplayRole].toString();
}
for (const QString &tableType : rosTables) {
if (tableType == "Images") {
QPoint initPos(0, 0);
auto *wgt = new CustomTableWidget;
auto *proxyControl = addRect(0, 0, 0, 0, QPen(Qt::black),
QBrush(Qt::darkGreen));
auto *sizeGrip = new QSizeGrip(wgt);
auto *layout = new QHBoxLayout(wgt);
layout->setContentsMargins(0, 0, 0, 0);
layout->addWidget(sizeGrip, 0, Qt::AlignRight | Qt::AlignBottom);
connect(wgt, &CustomTableWidget::sizeChanged, [wgt, proxyControl](){
proxyControl->setRect(wgt->geometry().adjusted(-10, -10, 10, 10));
});
wgt->setColumnCount(2);
wgt->setRowCount(2);
for (int ridx = 0; ridx < wgt->rowCount(); ridx++) {
for (int cidx = 0; cidx < wgt->columnCount(); cidx++) {
auto *item = new QTableWidgetItem();
item->setText(QString("%1").arg(ridx));
wgt->setItem(ridx,cidx,item);
}
}
auto *const proxy = addWidget(wgt);
proxy->setPos(initPos.x(), initPos.y()
+ proxyControl->rect().height());
proxy->setParentItem(proxyControl);
proxyControl->setPos(initPos.x(), initPos.y());
proxyControl->setFlag(QGraphicsItem::ItemIsMovable, true);
proxyControl->setFlag(QGraphicsItem::ItemIsSelectable, true);
proxyControl->setRect(wgt->geometry().adjusted(-10, -10, 10, 10));
}
}
}
What it was tried so far to solve the problem:
Now the QGraphicsScene have been subclassed to re-write the mouse events, in particular and in this case attention was given to the dropEvent(QGraphicsSceneDragDropEvent *event) as that is the responsible function to "see the widget"
Several trials and errors were conducted and in the same function it was tried to add the following part:
for (const QString &tableType : rosTables) {
if (tableType == "Images") {
QPoint initPos(event->scenePos()); // <-- Tried this but no change
auto *wgt = new CustomTableWidget;
auto *proxyControl = addRect(0, 0, 0, 0, QPen(Qt::black),
QBrush(Qt::darkGreen));
auto *sizeGrip = new QSizeGrip(wgt);
auto *layout = new QHBoxLayout(wgt);
}
An additional thing tried was to provide the QPoint the event->scenePos().toPoint() as deemed proper for the goal, but unfortunately that didn't solve the problem either:
for (const QString &tableType : rosTables) {
if (tableType == "Images") {
QPoint initPos(event->scenePos().toPoint()); // <-- Tried this too but no change
auto *wgt = new CustomTableWidget;
auto *proxyControl = addRect(0, 0, 0, 0, QPen(Qt::black),
QBrush(Qt::darkGreen));
auto *sizeGrip = new QSizeGrip(wgt);
auto *layout = new QHBoxLayout(wgt);
}
If anyone has an idea of what the problem might be please provide guidance on how to solve it.
Change proxy->setPos(initPos.x(), initPos.y() + proxyControl->rect().height()); to proxy->setPos(10, 10);.
Set scene rect:
Scene::Scene(QObject *parent) :
QGraphicsScene(parent)
{
setBackgroundBrush(Qt::lightGray);
setSceneRect(0, 0, 1000, 1000);
}
Set view alignment, e.g. in MainWindow.cpp:
ui->graphicsView->setAlignment(Qt::AlignLeft | Qt::AlignTop);
I think the title speaks for itself: Given a QTableWidget with items added by the setItem member function, I want to know what the margin is for each item. In particular, I want the width of the left margin in these cells.
I have prepared a little example computing text margins (a space between item rectangle and text content rectangle) of an item users clicks on:
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QMainWindow mainWin;
QTableWidget* table = new QTableWidget(3, 3, &mainWin);
table->setItem(0, 0, new QTableWidgetItem("Item A"));
table->setItem(1, 0, new QTableWidgetItem("Item B"));
table->setItem(2, 0, new QTableWidgetItem("Item C"));
table->setItem(0, 1, new QTableWidgetItem("Item D"));
table->setItem(1, 1, new QTableWidgetItem("Item E"));
table->setItem(2, 1, new QTableWidgetItem("Item F"));
table->setItem(0, 2, new QTableWidgetItem("Item G"));
table->setItem(1, 2, new QTableWidgetItem("Item H"));
table->setItem(2, 2, new QTableWidgetItem("Item I"));
mainWin.setCentralWidget(table);
mainWin.show();
auto slot = [&table](QTableWidgetItem* item){
QStyleOptionViewItem option;
option.font = item->font();
option.fontMetrics = QFontMetrics(item->font());
if (item->textAlignment())
option.displayAlignment = static_cast<Qt::Alignment>(item->textAlignment());
else
option.displayAlignment = Qt::AlignLeft | Qt::AlignVCenter; // default alignment
option.features |= QStyleOptionViewItem::HasDisplay;
option.text = item->text();
option.rect = table->visualItemRect(item);
// If your table cells contain also decorations or check-state indicators,
// you have to set also:
// option.features |= QStyleOptionViewItem::HasDecoration;
// option.icon = ...
// option.decorationSize = ...
QRect textRect = table->style()->subElementRect(QStyle::SE_ItemViewItemText, &option, nullptr);
double leftMargin = textRect.left() - option.rect.left();
double rightMargin = option.rect.right() - textRect.right();
double topMargin = textRect.top() - option.rect.top();
double bottomMargin = option.rect.bottom() - textRect.bottom();
qDebug() << leftMargin;
qDebug() << rightMargin;
qDebug() << topMargin;
qDebug() << bottomMargin;
};
QObject::connect(table, &QTableWidget::itemClicked, slot);
return app.exec();
}
EDIT
To compute an exact space between table cell border and the text pixels, you have to use a QFontMetrics class.
See the QFontMetrics::leftBearing() and QFontMetrics::tightBoundingRect().
I'm trying to create a BuildingTile class which has QGraphicsRectItem as base.
In this BuildingTile I'm trying to add QGraphicsEllipseItems and a QGraphicsSimpleTextItem but these do not use my BuildingTile's coordinate system although they say on http://doc.qt.io/qt-5/graphicsview.html: "Child coordinates are relative to the parent's coordinates. If the child is untransformed, the difference between a child coordinate and a parent coordinate is the same as the distance between the items in parent coordinates."
I would be really glad if someone could help me with this.
The header:
class BuildingTile : public QGraphicsRectItem
{
private:
Building* m_building;
bool m_empty;
QGraphicsSimpleTextItem* m_name;
QList<QGraphicsEllipseItem*> m_colonists;
public:
BuildingTile(qreal x, qreal y, QColor color, QString name, Building* m_building = 0);
bool isEmpty() const {return m_empty;}
void setEmpty(bool empty) {m_empty = empty;}
void setBuilding(Building* building) {m_building = building;}
};
The constructor:
BuildingTile::BuildingTile(qreal x, qreal y, QColor color, QString name, Building *building) : QGraphicsRectItem(x,y,150,75)
{
m_building = building;
setBrush(color);
for(int i = 0; i<3; i++)
{
QGraphicsEllipseItem* item = new QGraphicsEllipseItem(10+i*35, 40, 25, 25, this);
m_colonists.append(item);
item->setBrush(QColor(255,255,255));
}
m_name = new QGraphicsSimpleTextItem(name, this);
m_name->setPos(10,10);
}
MainWindow constructor:
MainWindow::MainWindow(QWidget *parent) : QWidget(parent)
{
QGraphicsScene* scene = new QGraphicsScene;
BuildingTile* item = new BuildingTile(0, 0, QColor(203,130,232), "small market");
scene->addItem(item);
item = new BuildingTile(150, 0, QColor(91,161,212), "indigo plant");
scene->addItem(item);
item = new BuildingTile(300, 0, QColor(120,113,107), "coffee roaster");
scene->addItem(item);
QGraphicsView* view = new QGraphicsView;
view->setScene(scene);
view->setAlignment(Qt::AlignTop | Qt::AlignLeft);
QHBoxLayout *layout = new QHBoxLayout;
layout->addWidget(view);
setLayout(layout);
}
All your BuildingTile items have their origin at the scene's origin, i.e. (0, 0) in scene coordinates.
For example (your second BuildingTile item):
item = new BuildingTile(150, 0, QColor(91,161,212), "indigo plant");
scene->addItem(item);
This creates a BuildingTile item located at (0, 0), containing a rectangle located at (150,0) of its own coordinate system. You're changing the position of the rectangle in its own coordinate system, but not the position of the rect's coordinate system in relation to its parent (the scene).
Now you create ellipses and labels in relation to the BuildingTile coordinate systems, which are all identical and located at (0,0) in "global" scene coordinates, so you end up with scene coordinates (10, 10) for all labels.
To achieve what you want, do:
item = new BuildingTile(0, 0, QColor(91,161,212), "indigo plant");
scene->addItem(item);
item->setPos(150, 0);
In my game, I'd like to fire rockets from a rocket launcher. The player holds a rocket launcher as a child item. The rockets must be parentless. I'm trying to position the rocket so that its back lines up with the back of the rocket launcher (the player is facing north in the screenshots) and centered horizontally within it:
Instead, what I'm getting is:
The rotation is also incorrect (run the example and move the mouse cursor around to see what I mean). Where am I going wrong in my code?
#include <QtWidgets>
QPointF moveBy(const QPointF &pos, qreal rotation, float distance)
{
return pos - QTransform().rotate(rotation).map(QPointF(0, distance));
}
float directionTo(const QPointF &source, const QPointF &target) {
QPointF toTarget(target.x() - source.x(), target.y() - source.y());
float facingTarget = qRadiansToDegrees(atan2(toTarget.y(), toTarget.x())) + 90.0f;
facingTarget = fmod(facingTarget, 360.0f);
if(facingTarget < 0)
facingTarget += 360.0f;
return facingTarget;
}
class Controller : public QObject
{
public:
Controller(QGraphicsScene *scene) :
mScene(scene)
{
mPlayer = scene->addRect(0, 0, 25, 25, QPen(Qt::blue));
mPlayer->setTransformOriginPoint(mPlayer->boundingRect().width() / 2, mPlayer->boundingRect().height() / 2);
mRocketLauncher = scene->addRect(0, 0, 16, 40, QPen(Qt::green));
mRocketLauncher->setParentItem(mPlayer);
mRocketLauncher->setPos(mPlayer->boundingRect().width() * 0.9 - mRocketLauncher->boundingRect().width() / 2,
-mRocketLauncher->boundingRect().height() * 0.3);
mRocket = scene->addRect(0, 0, 16, 20, QPen(Qt::red));
scene->installEventFilter(this);
QGraphicsTextItem *playerText = scene->addText("Player");
playerText->setPos(0, 100);
playerText->setDefaultTextColor(Qt::blue);
QGraphicsTextItem *rocketLauncherText = scene->addText("Rocket launcher");
rocketLauncherText->setPos(0, 120);
rocketLauncherText->setDefaultTextColor(Qt::green);
QGraphicsTextItem *rocketText = scene->addText("Rocket");
rocketText->setPos(0, 140);
rocketText->setDefaultTextColor(Qt::red);
}
bool eventFilter(QObject *, QEvent *event) {
if (event->type() == QEvent::GraphicsSceneMouseMove) {
const QGraphicsSceneMouseEvent *mouseEvent = static_cast<QGraphicsSceneMouseEvent*>(event);
mPlayer->setRotation(directionTo(mPlayer->sceneBoundingRect().center(), mouseEvent->scenePos()));
qreal rocketX = mRocketLauncher->sceneBoundingRect().center().x() - mRocket->boundingRect().width() / 2;
QPointF rocketPos(rocketX, 0);
rocketPos = moveBy(rocketPos, mPlayer->rotation(), mRocketLauncher->boundingRect().height() - mRocket->boundingRect().height());
mRocket->setPos(rocketPos);
mRocket->setRotation(mPlayer->rotation());
return true;
}
return false;
}
private:
QGraphicsScene *mScene;
QGraphicsRectItem *mPlayer;
QGraphicsRectItem *mRocketLauncher;
QGraphicsRectItem *mRocket;
};
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QGraphicsView view;
view.setMouseTracking(true);
QGraphicsScene *scene = new QGraphicsScene;
view.setScene(scene);
Controller controller(scene);
view.resize(300, 300);
view.show();
return app.exec();
}
The idea is to:
set rotation of both items;
get positions of bottom left corner of launcher and rocket in global (scene) coordinates;
shift rocket to make positinos equal.
Code:
mPlayer->setRotation(directionTo(mPlayer->sceneBoundingRect().center(),
mouseEvent->scenePos()));
mRocket->setRotation(mPlayer->rotation());
QPointF launcherPos = mRocketLauncher->mapToScene(
mRocketLauncher->boundingRect().bottomLeft());
QPointF currentRocketPos = mRocket->mapToScene(
mRocket->boundingRect().bottomLeft());
mRocket->setPos(mRocket->pos() - currentRocketPos + launcherPos);