How to animate line around fixed point in Qt? - c++

I'm trying to animate a line around a fixed point in Qt. I assume I need to use the QPropertyAnimation class to do this but cannot figure out which property to use.
For clarity, here's what I'm trying to do.
| (5, 10)
| /
| /
| /
| / (10, 5)
| / .
| /
| /
|/
|--------------------------
^
|---(0,0)
Given (x1, y1) = (0, 0) & (x2, y2) = (5, 10), this would be the first frame of the animation. I'd like to then do a smooth animation from (x1, y1), (x2, y2), (with (x1, y1) being one end of the line and (x2, y2) being the other end) to (x1, y1), (x3, y3), with (x3, y3) = (10, 5). Similar to how a clock hand is animated. And before someone posts the analog clock example it uses a rotating pixmap which is not what I need.
I haven't found a whole lot of information on Qt animations, just a lot of basic GUI tutorials.
I have tried doing the following
QPropertyAnimation *anim = new QPropertyAnimation(widget, "geometry")
and the problem with this method is that in this technique the widget is moved between 2 points based on (0, 0) of the widget using the ->setStartValue(startX, startY, ...) and does not allow me to keep one of my lines at a fixed point.
and
QPropertyAnimation *anim = new QPropertyAnimation(widget, "rotation")
The problem with this method being similar to geometry in that it rotates said widget along a (0, 0) point.
Can someone tell me how to achieve the desired effect?
Thanks.

QGraphicsXXXItem do not support q-properties so they can not be used with QPropertyAnimation directly. So the solution is to create a class that inherits QObject and QGraphicsLineItem, plus we must add a q-property that handles the p2 position of the QLineF associated with the line as shown below:
lineitem.h
#ifndef LINEITEM_H
#define LINEITEM_H
#include <QGraphicsLineItem>
#include <QObject>
class LineItem: public QObject, public QGraphicsLineItem {
Q_OBJECT
Q_PROPERTY(QPointF p1 READ p1 WRITE setP1)
Q_PROPERTY(QPointF p2 READ p2 WRITE setP2)
public:
using QGraphicsLineItem::QGraphicsLineItem;
QPointF p1() const {
return line().p1();
}
void setP1(const QPointF & p){
QLineF l = line();
l.setP1(p);
setLine(l);
}
QPointF p2() const {
return line().p2();
}
void setP2(const QPointF & p){
QLineF l = line();
l.setP2(p);
setLine(l);
}
};
#endif // LINEITEM_H
main.cpp
#include "lineitem.h"
#include <QApplication>
#include <QGraphicsView>
#include <QPropertyAnimation>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QGraphicsScene scene(-100, -100, 200, 200);
QGraphicsView view(&scene);
QGraphicsLineItem *item = scene.addLine(QLine(0, 100, 100, 0));
item->setPen(QPen(Qt::red, 5));
LineItem *lineItem = new LineItem(QLineF(QPointF(0, 0), QPointF(0, 100)));
scene.addItem(lineItem);
lineItem->setPen(QPen(Qt::green, 2));
QPropertyAnimation *anim = new QPropertyAnimation(lineItem, "p2");
anim->setStartValue(QPointF(0, 100));
anim->setEndValue(QPointF(100, 0));
anim->setDuration(2000);
anim->start();
view.resize(640, 480);
view.show();
return a.exec();
}
Another way is to use QVariantAnimation:
#include <QApplication>
#include <QGraphicsView>
#include <QPropertyAnimation>
#include <QGraphicsLineItem>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QGraphicsScene scene(-100, -100, 200, 200);
QGraphicsView view(&scene);
scene.addLine(QLine(0, 100, 100, 0), QPen(Qt::green));
QGraphicsLineItem *item = scene.addLine(QLine(0, 0, 0, 100));
item->setPen(QPen(Qt::red, 5));
QVariantAnimation * anim = new QVariantAnimation(&scene);
anim->setStartValue(QPointF(0, 100));
anim->setEndValue(QPointF(100, 0));
anim->setDuration(2000);
anim->start();
QObject::connect(anim, &QVariantAnimation::valueChanged, [item](const QVariant & val){
QLineF l = item->line();
l.setP2(val.toPointF());
item->setLine(l);
});
view.resize(640, 480);
view.show();
return a.exec();
}

Related

Why doesn't setPos() work when using addPolygon() method?

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"));
}

What exactly are the positioning rules for text in Qt?

Here's a very basic piece of code which:
Measures the size a piece of text would take.
Draws the rectangle which corresponds to this size at coordinates (100, 25).
Displays text at coordinates (100, 25).
auto str = "Hello, World!";
auto metrix = window->fontMetrics();
auto text = scene->addText(str);
text->setPos(100, 25);
text->setDefaultTextColor(Qt::white);
auto r = metrix.boundingRect(str);
int x, y, w, h;
r.getRect(&x, &y, &w, &h);
scene->addRect(100, 25, w, h, QPen(Qt::white));
The scene in code is a QGraphicsScene with no specific customizations, with the exception of a border set to zero.
I would expect the text to be exactly inside the rectangle. The text is however shifted by a few pixels to the left and a few more pixels to the bottom. Why?
Solution
Setting the document margins to 0, as #NgocMinhNguyen suggested, might seem to work, but it is not a real solution, because you lose the margins. It would be better, if you could get the actual geometry, including margins etc. For that purpose you can use QGraphicsTextItem::boundingRect() instead of QFontMetrics::boundingRect.
Example
Here is a minimal and complete example I have written for you, in order to demonstrate the proposed solution:
#include <QApplication>
#include <QGraphicsView>
#include <QGraphicsItem>
#include <QBoxLayout>
struct MainWindow : public QWidget
{
MainWindow(QWidget *parent = nullptr) : QWidget(parent) {
QPointF p(100, 25);
auto *l = new QVBoxLayout(this);
auto *view = new QGraphicsView(this);
auto *textItem = new QGraphicsTextItem(tr("HHHHHHHH"));
auto *rectItem = new QGraphicsRectItem(textItem->boundingRect()
.adjusted(0, 0, -1, -1));
textItem->setPos(p);
rectItem->setPos(p);
view->setScene(new QGraphicsScene(this));
view->scene()->addItem(textItem);
view->scene()->addItem(rectItem);
l->addWidget(view);
resize(300, 300);
}
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow w;
w.show();
return a.exec();
}
Note: Please note how I create the rectangle. There is a difference between
auto *item = new QGraphicsRectItem(100, 25, w, h);
and
auto *item = new QGraphicsRectItem(0, 0, w, h);
item->setPos(100, 25);
Result
This example produces the following result:
QGraphicsTextItem is held by QTextDocument, which can have a margin.
Setting the margin to 0 and the rectangle will be correctly drawn.
text->document()->setDocumentMargin(0);

How to change size of QGraphicsPixmap using animation?

I have class named Pixmap deriving from QGraphicsPixmapItem and StartScreen class deriving from QGraphicsScene. I want to use animations (QPropertyAnimation class) to resize displayed image in certain time range. Other actions like setting position or rotation aren't problem but I couldn't find any property like size (e.g. setSize() method). How can I do that in the other way? Thanks for advance.
StartScreen::StartScreen(int windowWidth, int windowHeight)
{
setSceneRect(0, 0, windowWidth, windowHeight);
setBackgroundBrush(QBrush(QImage(":/images/background.png")));
Pixmap * logo = new Pixmap(":/images/logo.png");
addItem(logo);
logo->setPos((windowWidth - logo->pixmap().width()) / 2, (windowWidth - logo->pixmap().width()) / 2 - 75);
//QPropertyAnimation * animation = new QPropertyAnimation(logo, "");
}
QPropertyAnimation applies to Qt Properties, but only objects that inherit from QObject have Qt properties, so if you want to use animations you can use QGraphicsObject and create your own item, or create a class that inherits the item you want and QObject.
class Pixmap: public QObject, public QGraphicsPixmapItem{
Q_OBJECT
Q_PROPERTY(qreal scale READ scale WRITE setScale)
Q_PROPERTY(qreal rotation READ rotation WRITE setRotation)
Q_PROPERTY(QPointF pos READ pos WRITE setPos)
public:
using QGraphicsPixmapItem::QGraphicsPixmapItem;
};
In the previous example, take advantage of the fact that QGraphicsItem, and therefore its derived classes, have the methods pos(), setPos(), scale(), setScale(), rotation() and setRotation(), so only use them in Q_PROPERTY.
In the next part I show an example:
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QGraphicsView w;
QGraphicsScene scene(0, 0, 640, 480);
w.setScene(&scene);
w.show();
Pixmap* logo = new Pixmap(QPixmap(":/image.jpg"));
scene.addItem(logo);
QSequentialAnimationGroup group;
QPropertyAnimation animation_scale(logo, "scale");
animation_scale.setDuration(1000);
animation_scale.setStartValue(2.0);
animation_scale.setEndValue(0.1);
QPropertyAnimation animation_pos(logo, "pos");
animation_pos.setDuration(1000);
animation_pos.setStartValue(QPointF(0, 0));
animation_pos.setEndValue(QPointF(100, 100));
/**
* it must indicate the center of rotation,
* in this case it will be the center of the item
*/
logo->setTransformOriginPoint(logo->boundingRect().center());
QPropertyAnimation animation_rotate(logo, "rotation");
animation_rotate.setDuration(1000);
animation_rotate.setStartValue(0);
animation_rotate.setEndValue(360);
group.addAnimation(&animation_scale);
group.addAnimation(&animation_pos);
group.addAnimation(&animation_rotate);
group.start();
return a.exec();
}
#include "main.moc"
In the following link there is an example
Or you can use QVariantAnimation instead of QPropertyAnimation:
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QGraphicsView w;
QGraphicsScene scene(0, 0, 640, 480);
w.setScene(&scene);
w.show();
QGraphicsPixmapItem* logo = new QGraphicsPixmapItem(QPixmap(":/image.jpg"));
scene.addItem(logo);
QSequentialAnimationGroup group;
QVariantAnimation animation_scale;
animation_scale.setDuration(1000);
animation_scale.setStartValue(2.0);
animation_scale.setEndValue(0.5);
QObject::connect(&animation_scale, &QVariantAnimation::valueChanged, [logo](const QVariant &value){
logo->setScale(value.toReal());
});
animation_scale.start();
QVariantAnimation animation_pos;
animation_pos.setDuration(1000);
animation_pos.setStartValue(QPointF(0, 0));
animation_pos.setEndValue(QPointF(100, 100));
QObject::connect(&animation_pos, &QVariantAnimation::valueChanged, [logo](const QVariant &value){
logo->setPos(value.toPointF());
});
/**
* it must indicate the center of rotation,
* in this case it will be the center of the item
*/
logo->setTransformOriginPoint(logo->boundingRect().center());
QVariantAnimation animation_rotate;
animation_rotate.setDuration(1000);
animation_rotate.setStartValue(0);
animation_rotate.setEndValue(360);
QObject::connect(&animation_rotate, &QVariantAnimation::valueChanged, [logo](const QVariant &value){
logo->setRotation(value.toReal());
});
group.addAnimation(&animation_scale);
group.addAnimation(&animation_pos);
group.addAnimation(&animation_rotate);
group.start();
return a.exec();
}
In the following link there is an example

Positioning a top-level object relative to another

I need to position a top-level object so that it always remains in a position relative to another top-level object. As an example, the rectangle in the image below should stick to the "front" of the ellipse:
When rotated 180 degrees, it should look like this:
Instead, the position of the rectangle is incorrect:
Please run the example below (the use of QGraphicsScene is for demonstration purposes only, as the actual use case is in physics).
#include <QtWidgets>
class Scene : public QGraphicsScene
{
Q_OBJECT
public:
Scene()
{
mEllipse = addEllipse(0, 0, 25, 25);
mEllipse->setTransformOriginPoint(QPointF(12.5, 12.5));
QGraphicsLineItem *line = new QGraphicsLineItem(QLineF(0, 0, 0, -12.5), mEllipse);
line->setPos(12.5, 12.5);
mRect = addRect(0, 0, 10, 10);
mRect->setTransformOriginPoint(QPointF(5, 5));
line = new QGraphicsLineItem(QLineF(0, 0, 0, -5), mRect);
line->setPos(5, 5);
connect(&mTimer, SIGNAL(timeout()), this, SLOT(timeout()));
mTimer.start(5);
}
public slots:
void timeout()
{
mEllipse->setRotation(mEllipse->rotation() + 0.5);
QTransform t;
t.rotate(mEllipse->rotation());
qreal relativeX = mEllipse->boundingRect().width() / 2 - mRect->boundingRect().width() / 2;
qreal relativeY = -mRect->boundingRect().height();
mRect->setPos(mEllipse->pos() + t.map(QPointF(relativeX, relativeY)));
mRect->setRotation(mEllipse->rotation());
}
public:
QTimer mTimer;
QGraphicsEllipseItem *mEllipse;
QGraphicsRectItem *mRect;
};
int main(int argc, char** argv)
{
QApplication app(argc, argv);
QGraphicsView view;
view.setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
view.setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
view.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform);
view.setScene(new Scene);
view.resize(200, 200);
view.show();
return app.exec();
}
#include "main.moc"
Note that the position of the rectangle is not always the same, but it should always remain in the same position relative to the ellipse. For example, it may start off in this position:
But it should stay in that relative position when rotated:
If you want the two objects to keep the same relative position, they need to rotate around the same origin point.
Here your circle rotates around its center (the point 12.5, 12.5), but your rectangle rotates around another origin (5,5) instead of the circle's center (12.5, 12.5).
If you fix the origin, it'll work as you expect:
mRect->setTransformOriginPoint(QPointF(12.5, 12.5));
Even if the rectangle starts off with an offset:
mRect = addRect(-10, 0, 10, 10); // Start 10 units to the left

Position parentless scene item under another scene item

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);