I'm using this solution to animate ellipses along a QPainterPath.
But I need to slowly increase or decrease the speed of the animation.
I create a timer and set a new duration for the animation but the result is a choppy animation because the ellipses start from the beginning.
Is there a better way to accomplish this?
class AnimatedEllipses: public QGraphicsObject
{
Q_OBJECT
Q_PROPERTY(int progress READ progress WRITE setProgress)
private:
QGraphicsPathItem path;
QList<QGraphicsEllipseItem*> ellipses;
int propProgress;
QPropertyAnimation* animation;
public:
int progress() const { return propProgress;}
void setProgress(int value)
{
propProgress = value;
int index = 0;
for (QGraphicsEllipseItem* ellipse: ellipses)
{
// Keep value between 0 and length.
int lgt = (propProgress + index * 40) % int(path.path().length());
qreal percent = path.path().percentAtLength(lgt);
++index;
ellipse->setPos(path.path().pointAtPercent(percent));
}
}
AnimatedEllipses(QPainterPath const& path): QGraphicsObject(), path(path), propProgress(0)
{
qreal pos = 0;
qreal length = path.length();
while (pos < length)
{
qreal percent = path.percentAtLength(pos);
QPointF pointAtPercent = path.pointAtPercent(percent);
pos += 40;
QGraphicsEllipseItem * item = new QGraphicsEllipseItem(-10, -10, 20, 20, this);
item->setPos(pointAtPercent);
ellipses << item;
}
animation = new QPropertyAnimation(this, "progress");
animation->setStartValue(0);
animation->setEndValue(length);
animation->setDuration(10000);
animation->setLoopCount(-1);
animation->start();
QTimer *timer = new QTimer();
connect(timer, SIGNAL(timeout()), this, SLOT(SlotTimeOut()));
timer->start(1000);
}
void SlotTimeOut() {
int newDuration = GetRandomDuration();
animation->setDuration(newDuration);
}
// QGraphicsItem interface
public:
QRectF boundingRect() const { return path.boundingRect();}
void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget){}
};
You can do this with custom QEasingCurve. Here is a small example for progressbar value.
MainWindow.h
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
private slots:
void OnChangeDurationTimer();
private:
Ui::MainWindow *ui;
QPropertyAnimation m_animProgress;
};
Initialization
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
m_animProgress.setTargetObject(ui->progressBar);
m_animProgress.setPropertyName("value");
m_animProgress.setStartValue(0);
m_animProgress.setEndValue(100);
m_animProgress.setDuration(10000);
m_animProgress.setLoopCount(-1);
//setting custom QEasingCurve
QEasingCurve eCurve;
eCurve.setCustomType(myEasingFunction);
m_animProgress.setEasingCurve(eCurve);
m_animProgress.start();
QTimer::singleShot(3000, this, &MainWindow::OnChangeDurationTimer); //timer to change duration
}
Most interesting is function myEasingFunction for custom EasingCurve
qreal g_offset = 0; //value last animation stopped with
qreal g_offsetLast = 0; //keep current value of animation
qreal myEasingFunction(qreal progress)
{
qreal val = g_offset + progress;
while (val > 1) {
val -= 1; //normalize
}
g_offsetLast = val;
return val;
}
And changing duration on timer
void MainWindow::OnChangeDurationTimer()
{
g_offset = g_offsetLast; //remember stopped value
m_animProgress.stop();
m_animProgress.setDuration((rand() % 10 + 1) * 1000);
m_animProgress.start();
QTimer::singleShot(3000, this, &MainWindow::OnChangeDurationTimer); //next changing
}
Related
I have a QListWidget and a QGraphicsView both subclassed to overwrite some of their members. I prepared a minimal verifiable example showing the problem I have here
From the QListWidget I can drag and drop specific field (represented by a QTableWidget) and drop them into a QGraphicsView and in order to do that I am using a QGraphicsProxyWidget approach as shown below.
The Problem
Now, how do I connect 2 QRadioButton inside cell of a QTableWidget with another cell of another QTableWidget?
It is important to mention that the green QGraphicsRectItem it is used to move around the QTableWidget as well as adjusting its dimension.
Below the result I was able to arrive so far:
And below the expected result I have been trying to achieve:
Below the most important part of 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
#include "arrow.h"
#include <QGraphicsSceneDragDropEvent>
#include <QMimeData>
#include <QTableWidget>
#include <QGraphicsProxyWidget>
#include <QVBoxLayout>
#include <QMetaEnum>
#include <QEvent>
#include <QSizeGrip>
#include <QRadioButton>
Scene::Scene(QObject *parent)
{
setBackgroundBrush(Qt::lightGray);
}
void Scene::dragEnterEvent(QGraphicsSceneDragDropEvent *event) {
if (event->mimeData()->hasFormat("application/x-qabstractitemmodeldatalist"))
event->setAccepted(true);
}
void Scene::dragMoveEvent(QGraphicsSceneDragDropEvent *event) {
if (event->mimeData()->hasFormat("application/x-qabstractitemmodeldatalist"))
event->setAccepted(true);
}
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(4);
wgt->setRowCount(4);
for (int ridx = 0; ridx < wgt->rowCount(); ridx++) {
for (int cidx = 0; cidx < wgt->columnCount(); cidx++) {
QRadioButton *radio1, *radio2;
auto* item = new QTableWidgetItem();
item->setText(QString("%1").arg(ridx));
wgt->setItem(ridx,cidx,item);
radio1 = new QRadioButton;
radio2 = new QRadioButton;
wgt->setCellWidget(cidx, 0, radio1);
wgt->setCellWidget(cidx, 3, radio2);
Arrow *arrow = new Arrow;
}
}
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));
}
}
}
diagramitem.h
#ifndef DIAGRAMITEM_H
#define DIAGRAMITEM_H
#include <QGraphicsPolygonItem>
class Arrow;
class DiagramItem : public QGraphicsPolygonItem
{
public:
DiagramItem(QMenu *contextMenu, QGraphicsItem *parent = Q_NULLPTR);
void removeArrow(Arrow *arrow);
void removeArrows();
void addArrow(Arrow *arrow);
QPixmap image() const;
protected:
void contextMenuEvent(QGraphicsSceneContextMenuEvent *event) override;
QVariant itemChange(GraphicsItemChange change, const QVariant &value) override;
private:
QPolygonF myPolygon;
QList<Arrow*> arrows;
QMenu *myContextMenu;
};
#endif // DIAGRAMITEM_H
diagramitem.cpp
#include "diagramitem.h"
#include "arrow.h"
#include <QPainter>
#include <QGraphicsScene>
#include <QGraphicsSceneContextMenuEvent>
#include <QMenu>
DiagramItem::DiagramItem(QMenu *contextMenu, QGraphicsItem *parent) : QGraphicsPolygonItem(parent)
{
myContextMenu = contextMenu;
setPolygon(myPolygon);
setFlag(QGraphicsItem::ItemIsMovable, true);
setFlag(QGraphicsItem::ItemIsSelectable, true);
setFlag(QGraphicsItem::ItemSendsGeometryChanges, true);
}
void DiagramItem::removeArrow(Arrow *arrow)
{
int index = arrows.indexOf(arrow);
if (index != -1)
arrows.removeAt(index);
}
void DiagramItem::removeArrows()
{
foreach (Arrow *arrow, arrows) {
arrow->startItem()->removeArrow(arrow);
arrow->endItem()->removeArrow(arrow);
scene()->removeItem(arrow);
delete arrow;
}
}
void DiagramItem::addArrow(Arrow *arrow)
{
arrows.append(arrow);
}
void DiagramItem::contextMenuEvent(QGraphicsSceneContextMenuEvent *event)
{
scene()->clearSelection();
setSelected(true);
myContextMenu->exec(event->screenPos());
}
QVariant DiagramItem::itemChange(QGraphicsItem::GraphicsItemChange change, const QVariant &value)
{
if (change == QGraphicsItem::ItemPositionChange) {
foreach (Arrow *arrow, arrows) {
arrow->updatePosition();
}
}
return value;
}
arrow.h
#ifndef ARROW_H
#define ARROW_H
#include <QGraphicsLineItem>
#include "diagramitem.h"
class Arrow : public QGraphicsLineItem
{
public:
enum { Type = UserType + 4 };
Arrow(DiagramItem *startItem, DiagramItem *endItem,
QGraphicsItem *parent = nullptr);
DiagramItem *startItem() const { return myStartItem; }
DiagramItem *endItem() const { return myEndItem; }
QPainterPath shape() const override;
void setColor(const QColor &color) {
myColor = color;
}
int type() const override { return Type; }
void updatePosition();
protected:
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = nullptr) override;
private:
QColor myColor;
DiagramItem *myStartItem;
DiagramItem *myEndItem;
QPolygonF arrowHead;
};
#endif // ARROW_H
arrow.cpp
#include "arrow.h"
#include <QPen>
#include <QPainter>
#include "qmath.h"
Arrow::Arrow(DiagramItem *startItem, DiagramItem *endItem, QGraphicsItem *parent) : QGraphicsLineItem(parent)
{
myStartItem = startItem;
myEndItem = endItem;
myColor = Qt::GlobalColor::black;
setPen(QPen(myColor, 2, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));
setFlag(QGraphicsItem::ItemIsSelectable, true);
}
QPainterPath Arrow::shape() const
{
QPainterPath path = QGraphicsLineItem::shape();
path.addPolygon(arrowHead);
return path;
}
void Arrow::updatePosition()
{
QLineF line(mapFromItem(myStartItem, 0, 0), mapFromItem(myEndItem, 0, 0));
setLine(line);
}
void Arrow::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
Q_UNUSED(option)
Q_UNUSED(widget)
if (myStartItem->collidesWithItem(myEndItem))
return;
QPen myPen = pen();
myPen.setColor(myColor);
qreal arrowSize = 20;
painter->setPen(myPen);
painter->setBrush(myColor);
QLineF centerLine(myStartItem->pos(), myEndItem->pos());
QPolygonF endPolygon = myEndItem->polygon();
QPointF p1 = endPolygon.first() + myEndItem->pos();
QPointF p2;
QPointF intersectPoint;
QLineF polyLine;
for (int i = 1; i < endPolygon.count(); ++i) {
p2 = endPolygon.at(i) + myEndItem->pos();
polyLine = QLineF(p1, p2);
QLineF::IntersectType intersectType =
polyLine.intersect(centerLine, &intersectPoint);
if (intersectType == QLineF::BoundedIntersection)
break;
p1 = p2;
}
setLine(QLineF(intersectPoint, myStartItem->pos()));
double angle = std::atan2(-line().dy(), line().dx());
QPointF arrowP1 = line().p1() + QPointF(sin(angle + M_PI / 3) * arrowSize,
cos(angle + M_PI / 3) * arrowSize);
QPointF arrowP2 = line().p1() + QPointF(sin(angle + M_PI - M_PI / 3) * arrowSize,
cos(angle + M_PI - M_PI / 3) * arrowSize);
arrowHead.clear();
arrowHead << line().p1() << arrowP1 << arrowP2;
painter->drawLine(line());
painter->drawPolygon(arrowHead);
if (isSelected()) {
painter->setPen(QPen(myColor, 1, Qt::DashLine));
QLineF myLine = line();
myLine.translate(0, 4.0);
painter->drawLine(myLine);
myLine.translate(0,-8.0);
painter->drawLine(myLine);
}
}
What I have done so far to solve the problem:
1) I came across this post which was useful to understand the initial idea on how to do that, but it didn't really provide a way, or an implementation idea on how to best proceed
2) I researched the official documentation and before asking this question I went through the whole Diagram Scene example provided and understood how to create an Arrow object. The documentation about that was very good and made me understand how the graphics line item has to be formed.
However I was not able (coming back to my example) how to make "aware" the QRadioButton that I am trying to use its center as starting point for an arrow ad, therefore, how do I make "aware" the destination QRadioButton in another cell that it has to be connected there?
Below a particular of what I mean:
So basically the start point of the QRadioButton change color (or style) and the arrival point also change color.
3) I thought that the Arrow object has to be created inside the subclassed QGraphicsScene since it already handles the mouse events.
4) Despite what I tried so far I could not find any other useful help. Although I am still investigating how to do that.
If anyone has ever been in the same situation please provide guidance on how to better proceed to solve this problem and find a solution to this issue.
Solution
When a start and end radio buttons are checked, you need to create the arrow with those buttons as start and end nodes, e.g.:
void Backend::onInputRadioButton(bool checked)
{
m_endNode = checked ? static_cast<QRadioButton *>(sender()) : nullptr;
if (m_startNode && m_endNode)
m_scene->addItem(new ArrowItem(m_startNode, m_endNode));
}
Then you need to connect the signal of the top-most graphics items, which hold the tables, with the updatePosition slot of the ArrowItem, e.g.:
connect(m_startItem->property("item").value<MovableItem *>(),
&MovableItem::itemMoved, this, &ArrowItem::updatePosition);
connect(m_endItem->property("item").value<MovableItem *>(),
&MovableItem::itemMoved, this, &ArrowItem::updatePosition);
Note: I am using a property to hold a reference to the container item.
Finally, you need to update the arrow line, e.g.:
void ArrowItem::updatePosition()
{
QPointF offset(7, 15);
QPointF p1 = m_startItem->property("item").value<MovableItem *>()->pos()
+ m_startItem->parentWidget()->mapToParent(m_startItem->pos())
+ offset;
QPointF p2 = m_endItem->property("item").value<MovableItem *>()->pos()
+ m_endItem->parentWidget()->mapToParent(m_endItem->pos())
+ offset;
setLine(QLineF(p1, p2));
}
Example
I have dared to suggest improvements in your code. You can find the complete example I wrote for you on GitHub.
Result
The provided example produces the following result:
Note: The arrow heads are missing. Check once again the Diagram Scene Example to get an idea of how to draw them.
I have a bunch of ellipses that initially are lined on top of a path and should move along the QPainterPath. I have it working for the first ellipse but I can't figure out how to get the correct position for the other ellipses.
Is there a way to check if it passed the end of the path and move it back to the beginning?
class Animation : public QAbstractAnimation
{
public:
Animation(const QPainterPath& path, QObject *parent = Q_NULLPTR);
virtual void updateCurrentTime(int ms) override;
virtual int duration() const override;
QPainterPath mPath;
QVector<EllipseGraphicsItem*> mAnimationElements;
};
Animation::Animation (const QPainterPath& path, QObject *parent) : QAbstractAnimation(parent)
, mPath(path)
{
qreal pos = 0;
qreal length = mPath.length();
while (pos < length)
{
qreal percent = path.percentAtLength(pos);
QPointF pointAtPercent = path.pointAtPercent(percent);
pos += 40;
EllipseGraphicsItem * item = new EllipseGraphicsItem(parentItem());
mAnimationElements.append(item);
item->setPos(pointAtPercent);
}
}
void Animation::updateCurrentTime(int ms)
{
QPointF point = mPath.pointAtPercent(qreal(ms) / 6000);
if (mAnimationElements.size() > 0)
mAnimationElements[0]->setPos(point);
for (int i = 0; i < mAnimationElements.size(); i++) {
// how to update each circle's position?
}
}
Start the animation:
QPainterPath path;
path.moveTo(10, 10);
path.lineTo(QPointF(500, 10));
path.lineTo(QPointF(500, 700));
path.lineTo(QPointF(10, 700));
Animation *animation = new Animation(path, this);
animation->setLoopCount(-1);
animation->start();
Imho, it would be easier to use a QGraphicsObject with a QPropertyAnimation:
Use a property that varies between 0 and the length of the path and place your elements by calculating their positions from its value and their position in the list.
A quick example :
class AnimatedEllipses: public QGraphicsObject
{
Q_OBJECT
Q_PROPERTY(int progress READ progress WRITE setProgress)
private:
QGraphicsPathItem path;
QList<QGraphicsEllipseItem*> ellipses;
int propProgress;
public:
int progress() const { return propProgress;}
void setProgress(int value)
{
propProgress = value;
int index = 0;
for (QGraphicsEllipseItem* ellipse: ellipses)
{
// Keep value between 0 and length.
int lgt = (propProgress + index * 40) % int(path.path().length());
qreal percent = path.path().percentAtLength(lgt);
++index;
ellipse->setPos(path.path().pointAtPercent(percent));
}
}
AnimatedEllipses(QPainterPath const& path): QGraphicsObject(), path(path), propProgress(0)
{
qreal pos = 0;
qreal length = path.length();
while (pos < length)
{
qreal percent = path.percentAtLength(pos);
QPointF pointAtPercent = path.pointAtPercent(percent);
pos += 40;
QGraphicsEllipseItem * item = new QGraphicsEllipseItem(-10, -10, 20, 20, this);
item->setPos(pointAtPercent);
ellipses << item;
}
QPropertyAnimation* animation = new QPropertyAnimation(this, "progress");
animation->setStartValue(0);
animation->setEndValue(length);
animation->setDuration(10000);
animation->setLoopCount(-1);
animation->start();
}
// QGraphicsItem interface
public:
QRectF boundingRect() const { return path.boundingRect();}
void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget){}
};
The modulo allows you to create an infinte loop for each ellipse.
My idea in this project is to perform swap animation on items.
Problem is however that when I perform swap on items for the first time they keep their position still, but when the other animation starts that involves already swapped items, those items fall back to their initial positions. Please tell me what am I doing wrong. Animation as follows:
#include <QtCore>
#include <QtWidgets>
/**
* Element to be displayed in QGraphicsView
*/
class QGraphicsRectWidget : public QGraphicsWidget
{
Q_OBJECT
int m_number;
public:
void changePosition(QGraphicsRectWidget *other)
{
setPos(mapToParent(other->x() < x() ? -abs(x() - other->x())
: abs(x() - other->x()) ,0));
}
static int NUMBER;
QGraphicsRectWidget(QGraphicsItem *parent = 0) : QGraphicsWidget(parent), m_number(NUMBER)
{ NUMBER++;}
void paint(QPainter *painter, const QStyleOptionGraphicsItem *,
QWidget *) Q_DECL_OVERRIDE
{
painter->fillRect(rect(), QColor(127, 63, 63));
painter->drawText(rect(), QString("%1").arg(m_number), QTextOption(Qt::AlignCenter));
}
};
int QGraphicsRectWidget::NUMBER = 1;
class MyAnim : public QPropertyAnimation
{
Q_OBJECT
QGraphicsView &pview; // View in which elements must be swapped
int i1, i2; // Indices for elements to be swapped
public:
MyAnim(QGraphicsView &view, int index1 = 0, int index2 = 1, QObject *par = 0)
: QPropertyAnimation(par), pview(view), i1(index1), i2(index2)
{
QObject::connect(this, SIGNAL(finished()), SLOT(slotOnFinish()));
}
public slots:
/* !!!!!!!!!!!!!!!!!!!!!!! HERE IS THE PROBLEM (brobably) !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!*/
// Triggered when animation is over and sets position of target element to position of its end value
void slotOnFinish()
{
auto list = pview.items();
static_cast<QGraphicsRectWidget*>(list.at(i1))
->changePosition(static_cast<QGraphicsRectWidget*>(list.at(i2)));
}
};
class GraphicsView : public QGraphicsView
{
Q_OBJECT
public:
GraphicsView(QGraphicsScene *scene, QWidget *parent = NULL) : QGraphicsView(scene, parent)
{
}
protected:
virtual void resizeEvent(QResizeEvent *event) Q_DECL_OVERRIDE
{
fitInView(scene()->sceneRect());
QGraphicsView::resizeEvent(event);
}
};
#define SWAP_HEIGHT 75
/**
* Creates swap animation for items in QGraphicsView
*/
QParallelAnimationGroup* getSwapAnimation(QGraphicsView &view, int noItem1, int noItem2)
{
auto list = view.items();
QGraphicsRectWidget *wgt1 = static_cast<QGraphicsRectWidget*>(list.at(noItem1));
QGraphicsRectWidget *wgt2 = static_cast<QGraphicsRectWidget*>(list.at(noItem2));
MyAnim *pupperAnim, *plowerAnim;
QParallelAnimationGroup *par = new QParallelAnimationGroup;
plowerAnim = new MyAnim(view, noItem1, noItem2);
plowerAnim->setTargetObject(wgt2);
plowerAnim->setPropertyName("pos");
plowerAnim->setDuration(5000);
plowerAnim->setKeyValueAt(1.0/3.0, QPoint(wgt2->x(), wgt1->y() - SWAP_HEIGHT));
plowerAnim->setKeyValueAt(2.0/3.0, QPoint(wgt1->x(), wgt1->y() - SWAP_HEIGHT));
plowerAnim->setEndValue(wgt1->pos());
pupperAnim = new MyAnim(view, noItem2, noItem1);
pupperAnim->setTargetObject(wgt1);
pupperAnim->setPropertyName("pos");
pupperAnim->setDuration(5000);
pupperAnim->setKeyValueAt(1.0/3.0, QPoint(wgt1->x(), wgt2->y() + SWAP_HEIGHT));
pupperAnim->setKeyValueAt(2.0/3.0, QPoint(wgt2->x(), wgt2->y() + SWAP_HEIGHT));
pupperAnim->setEndValue(wgt2->pos());
par->addAnimation(pupperAnim);
par->addAnimation(plowerAnim);
return par;
}
int main(int argc, char **argv)
{
QApplication app(argc, argv);
QGraphicsRectWidget *button1 = new QGraphicsRectWidget;
QGraphicsRectWidget *button2 = new QGraphicsRectWidget;
QGraphicsRectWidget *button3 = new QGraphicsRectWidget;
QGraphicsRectWidget *button4 = new QGraphicsRectWidget;
button2->setZValue(1);
button3->setZValue(2);
button4->setZValue(3);
QGraphicsScene scene(0, 0, 300, 300);
scene.setBackgroundBrush(QColor(23, 0, 0));
scene.addItem(button1);
scene.addItem(button2);
scene.addItem(button3);
scene.addItem(button4);
GraphicsView window(&scene);
window.setFrameStyle(0);
window.setAlignment(Qt::AlignLeft | Qt::AlignTop);
window.setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
window.setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
QList<QGraphicsItem*> items = window.items();
QPoint start(20, 125);
for (auto item : items) // Set items in initial position
{
QGraphicsWidget *wgt = static_cast<QGraphicsWidget*>(item);
wgt->resize(50,50);
wgt->moveBy(start.x(), start.y());
start.setX(start.x() + 70);
}
QSequentialAnimationGroup gr;
gr.addAnimation(getSwapAnimation(window, 0, 1));
gr.addAnimation(getSwapAnimation(window, 1, 2));
gr.addAnimation(getSwapAnimation(window, 2, 3));
gr.addAnimation(getSwapAnimation(window, 3, 1));
gr.start();
window.resize(300, 300);
window.show();
return app.exec();
}
#include "main.moc"
UPD: Don't use animation with that purpose
UPD*: Forget previous UPD
Your animation retains the position of the items involved at the time the animation is created. By the time the second animation runs, this information is invalid.
You need to redesign the animation to update its keypoint values at the time it starts. You might also wish to ensure that the animated items run at a constant speed - or at least at a speed you have full control over.
For example:
// https://github.com/KubaO/stackoverflown/tree/master/questions/scene-anim-swap-40787655
#include <QtWidgets>
#include <cmath>
class QGraphicsRectWidget : public QGraphicsWidget
{
public:
void paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) override
{
painter->fillRect(rect(), Qt::blue);
painter->setPen(Qt::yellow);
painter->drawText(rect(), QString::number(zValue()), QTextOption(Qt::AlignCenter));
}
};
class SwapAnimation : public QPropertyAnimation
{
QPointer<QObject> other;
qreal offset;
QPoint propertyOf(QObject *obj) {
return obj->property(propertyName().constData()).toPoint();
}
void updateState(State newState, State oldState) override {
if (newState == Running && oldState == Stopped) {
auto start = propertyOf(targetObject());
auto end = propertyOf(other);
auto step1 = fabs(offset);
auto step2 = QLineF(start,end).length();
auto steps = 2.0*step1 + step2;
setStartValue(start);
setKeyValueAt(step1/steps, QPoint(start.x(), start.y() + offset));
setKeyValueAt((step1+step2)/steps, QPoint(end.x(), end.y() + offset));
setEndValue(end);
setDuration(10.0 * steps);
}
QPropertyAnimation::updateState(newState, oldState);
}
public:
SwapAnimation(QObject *first, QObject *second, qreal offset) : other(second), offset(offset) {
setTargetObject(first);
setPropertyName("pos");
}
};
QParallelAnimationGroup* getSwapAnimation(QObject *obj1, QObject *obj2)
{
auto const swapHeight = 75.0;
auto par = new QParallelAnimationGroup;
par->addAnimation(new SwapAnimation(obj2, obj1, -swapHeight));
par->addAnimation(new SwapAnimation(obj1, obj2, swapHeight));
return par;
}
int main(int argc, char **argv)
{
QApplication app(argc, argv);
QGraphicsScene scene(0, 0, 300, 300);
QGraphicsRectWidget buttons[4];
int i = 0;
QPointF start(20, 125);
for (auto & button : buttons) {
button.setZValue(i++);
button.resize(50,50);
button.setPos(start);
start.setX(start.x() + 70);
scene.addItem(&button);
}
QSequentialAnimationGroup gr;
gr.addAnimation(getSwapAnimation(&buttons[0], &buttons[1]));
gr.addAnimation(getSwapAnimation(&buttons[1], &buttons[2]));
gr.addAnimation(getSwapAnimation(&buttons[2], &buttons[3]));
gr.addAnimation(getSwapAnimation(&buttons[3], &buttons[1]));
gr.start();
QGraphicsView view(&scene);
view.setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
view.setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
view.resize(300, 300);
view.show();
return app.exec();
}
When I want to create a new Qt Quick Item in C++ in I extend QQuickItem. It happens that I want my new item to have the same basic properties of a Rectangle. Is there any class I can extend to have a Rectangle directly?
Rectangle is actually QQuickRectangle, but it is not exported to be used in C++. The header file is QtQuick\private\qquickrectangle_p.h.
Check my version of an AdvancedRectangle that can have a different radius for each corner. I had to re-implement Rectangle's behavior.
advancedrectangle.h
#ifndef ADVANCEDRECTANGLE_H
#define ADVANCEDRECTANGLE_H
#include <QQuickPaintedItem>
#include <QPainter>
#include <QPen>
class AdvancedRectangle : public QQuickPaintedItem
{
Q_OBJECT
public:
explicit AdvancedRectangle(QQuickItem *parent = 0);
Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged)
QColor color() const;
void setColor(QColor color);
Q_PROPERTY(qreal radius READ radius WRITE setRadius NOTIFY radiusChanged)
qreal radius() const;
void setRadius(qreal r);
Q_PROPERTY(qreal radiusTopLeft READ radiusTopLeft WRITE setRadiusTopLeft NOTIFY radiusTopLeftChanged)
qreal radiusTopLeft() const;
void setRadiusTopLeft(qreal r);
Q_PROPERTY(qreal radiusTopRight READ radiusTopRight WRITE setRadiusTopRight NOTIFY radiusTopRightChanged)
qreal radiusTopRight() const;
void setRadiusTopRight(qreal r);
Q_PROPERTY(qreal radiusBottomRight READ radiusBottomRight WRITE setRadiusBottomRight NOTIFY radiusBottomRightChanged)
qreal radiusBottomRight() const;
void setRadiusBottomRight(qreal r);
Q_PROPERTY(qreal radiusBottomLeft READ radiusBottomLeft WRITE setRadiusBottomLeft NOTIFY radiusBottomLeftChanged)
qreal radiusBottomLeft() const;
void setRadiusBottomLeft(qreal r);
void paint(QPainter *painter);
enum START_ANGELS {
START_E = 0,
START_N = 90,
START_W = 180,
START_S = 270
};
signals:
void colorChanged();
void radiusChanged();
void radiusTopLeftChanged();
void radiusTopRightChanged();
void radiusBottomRightChanged();
void radiusBottomLeftChanged();
public slots:
private:
QColor color_ = QColor(255, 255, 255);
qreal radius_ = 0.0;
qreal radiusTopLeft_ = -1;
qreal radiusTopRight_ = -1;
qreal radiusBottomRight_ = -1;
qreal radiusBottomLeft_ = -1;
private slots:
void onAppearanceChanged();
};
#endif // ADVANCEDRECTANGLE_H
advancedrectangle.cpp
#include "advancedrectangle.h"
AdvancedRectangle::AdvancedRectangle(QQuickItem *parent) :
QQuickPaintedItem(parent)
{
connect(this, SIGNAL(widthChanged()), SLOT(onAppearanceChanged()));
connect(this, SIGNAL(heightChanged()), SLOT(onAppearanceChanged()));
connect(this, SIGNAL(colorChanged()), SLOT(onAppearanceChanged()));
}
QColor AdvancedRectangle::color() const
{
return color_;
}
void AdvancedRectangle::setColor(QColor color)
{
if (color_ == color) return;
color_ = color;
emit colorChanged();
}
qreal AdvancedRectangle::radius() const
{
return radius_;
}
void AdvancedRectangle::setRadius(qreal r)
{
if (r == radius_) return;
radius_ = r;
emit radiusChanged();
}
qreal AdvancedRectangle::radiusTopLeft() const
{
if (radiusTopLeft_ >= 0) return radiusTopLeft_;
else return radius_;
}
void AdvancedRectangle::setRadiusTopLeft(qreal r)
{
if (r == radiusTopLeft_) return;
radiusTopLeft_ = r;
emit radiusTopLeftChanged();
}
qreal AdvancedRectangle::radiusTopRight() const
{
if (radiusTopRight_ >= 0) return radiusTopRight_;
else return radius_;
}
void AdvancedRectangle::setRadiusTopRight(qreal r)
{
if (r == radiusTopRight_) return;
radiusTopRight_ = r;
emit radiusTopRightChanged();
}
qreal AdvancedRectangle::radiusBottomRight() const
{
if (radiusBottomRight_ >= 0) return radiusBottomRight_;
else return radius_;
}
void AdvancedRectangle::setRadiusBottomRight(qreal r)
{
if (r == radiusBottomRight_) return;
radiusBottomRight_ = r;
emit radiusBottomRightChanged();
}
qreal AdvancedRectangle::radiusBottomLeft() const
{
if (radiusBottomLeft_ >= 0) return radiusBottomLeft_;
else return radius_;
}
void AdvancedRectangle::setRadiusBottomLeft(qreal r)
{
if (r == radiusBottomLeft_) return;
radiusBottomLeft_ = r;
emit radiusBottomLeftChanged();
}
void AdvancedRectangle::paint(QPainter *painter)
{
QPen pen(Qt::NoPen);
painter->setPen(pen);
QBrush brush(color_);
painter->setBrush(brush);
painter->setRenderHints(QPainter::Antialiasing, true);
int _width = width();
int _height = height();
QRectF rectangle;
// top-left
rectangle = QRectF(0, 0, 2*radiusTopLeft(), 2*radiusTopLeft());
painter->drawPie(rectangle, START_W * 16, 90 * -1 * 16);
// top-right
rectangle = QRectF(_width-2*radiusTopRight(), 0, 2*radiusTopRight(), 2*radiusTopRight());
painter->drawPie(rectangle, START_N * 16, 90 * -1 * 16);
// bottom-right
rectangle = QRectF(_width-2*radiusBottomRight(), _height-2*radiusBottomRight(), 2*radiusBottomRight(), 2*radiusBottomRight());
painter->drawPie(rectangle, START_E * 16, 90 * -1 * 16);
// bottom-left
rectangle = QRectF(0, _height-2*radiusBottomLeft(), 2*radiusBottomLeft(), 2*radiusBottomLeft());
painter->drawPie(rectangle, START_S * 16, 90 * -1 * 16);
QPointF points[12] = {
QPointF(radiusTopLeft(), 0),
QPointF(radiusTopLeft(), radiusTopLeft()),
QPointF(0, radiusTopLeft()),
QPointF(0, _height-radiusBottomLeft()),
QPointF(radiusBottomLeft(), _height-radiusBottomLeft()),
QPointF(radiusBottomLeft(), _height),
QPointF(_width-radiusBottomRight(), _height),
QPointF(_width-radiusBottomRight(), _height-radiusBottomRight()),
QPointF(_width, _height-radiusBottomRight()),
QPointF(_width, radiusTopRight()),
QPointF(_width-radiusTopRight(), radiusTopRight()),
QPointF(_width-radiusTopRight(), 0)
};
painter->drawPolygon(points, 12);
}
void AdvancedRectangle::onAppearanceChanged()
{
int new_width = width();
int new_height = height();
update(QRect(0, 0, new_width, new_height));
}
Let me know if you need that MIT licensed.
In this program I have built a QPropertyanimation and add to it my item and pos() property.
I override KeyPressEvent. And with using of keys consist of j, f, z item go forward ,go back and jump.
According gravity when item jump should fall. For this purpose I call down function. But item just once jump don't fall. I also have another problem: when the first press j and f (forward and back) item animate desirably but for next times item go forward and go back all of scene.
I mean It should animated for example 40 pixel but It animated 800 pixel.
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = 0);
QPoint start;
QPoint end;
~MainWindow();
private:
QGraphicsView* view;
QGraphicsScene* scene;
void keyPressEvent(QKeyEvent* k);
MyQgraphicsObject* m;
QPropertyAnimation* pr;
QElapsedTimer* timer;
int f;
int u;
int b;
void forward();
void up();
void back();
void down();
};
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent)
{
view=new QGraphicsView;
scene=new QGraphicsScene;
m=new MyQgraphicsObject;
pr=new QPropertyAnimation(m,"pos");
view->setScene(scene);
view->resize(800,800);
view->setFixedSize(800,800);
setCentralWidget(view);
scene->addItem(m);
start= QPoint(0,0);
f=30;
u=-30;
b=-30;
}
void MainWindow::keyPressEvent(QKeyEvent *k)
{
switch (k->key()) {
case Qt::Key_J: {
forward();
break;
}
case Qt::Key_Z: {
up();
down();
break;
}
case Qt::Key_F: {
back();
break;
}
default:
break;
}
}
void MainWindow::forward()
{
end.setX(f);
pr->setEndValue(end);
pr->setDuration(1000);
pr->setEasingCurve(QEasingCurve::Linear);
pr->start();
f+=40;
}
void MainWindow::up()
{
end.setY(u);
pr->setEndValue(end);
pr->setDuration(1000);
pr->setEasingCurve(QEasingCurve::Linear);
pr->start();
u-=30;
pr->pause();
}
void MainWindow::back()
{
end.setX(b);
pr->setEndValue(end);
pr->setDuration(1000);
pr->setEasingCurve(QEasingCurve::Linear);
pr->start();
b-=40;
}
void MainWindow::down()
{
u+=30;
end.setY(u);
pr->setEndValue(end);
pr->setDuration(1000);
pr->setEasingCurve(QEasingCurve::Linear);
pr->start();
}
You should not use resize and setFixedSize on view because you use it in setCentralWidget and its size will be managed by the layout. You should use setFixedSize on main window instead.
Animations are asynchonous. For example, when you call up(); down(), these functions will be executed without 1-second pause. Also, starting animation when it's already started has no effect.
Usually animations are used in such way when you know exactly where you need object to move in the next second. It's complicated to receive directives from user and change object's trajectory when the animation is already performing.
Here is an example showing correct use of animations for this task. Object can receive one directive (forward, back or jump) per second, and according animation will be performed in the next second.
Header:
class MainWindow : public QMainWindow {
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
private:
QGraphicsView* view;
QGraphicsScene* scene;
void keyPressEvent(QKeyEvent* k);
QGraphicsObject* object;
QPropertyAnimation* animation;
QPointF pos;
double speed;
enum Command {
command_none, command_jump, command_forward, command_back
};
Command next_command;
private slots:
void timeout();
};
Source:
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent)
{
view = new QGraphicsView;
scene = new QGraphicsScene;
object = scene->addWidget(new QPushButton("test"));
object->setPos(0, -object->boundingRect().height());
animation = new QPropertyAnimation(object,"pos");
animation->setDuration(1000);
view->setScene(scene);
setFixedSize(800,800);
scene->addRect(-500, -200, 1000, 200);
setCentralWidget(view);
scene->addItem(object);
next_command = command_none;
speed = 100;
QTimer* timer = new QTimer(this);
connect(timer, SIGNAL(timeout()), this, SLOT(timeout()));
timer->start(1000);
}
MainWindow::~MainWindow() {}
void MainWindow::keyPressEvent(QKeyEvent *k)
{
switch (k->key()) {
case Qt::Key_J: {
next_command = command_forward;
break;
}
case Qt::Key_Z: {
next_command = command_jump;
break;
}
case Qt::Key_F: {
next_command = command_back;
break;
}
default:
break;
}
}
void MainWindow::timeout() {
//fall
if (pos.y() < 0) {
pos.setY(pos.y() + speed);
if (pos.y() >= 0) {
pos.setY(0);
}
}
//action
switch(next_command) {
case command_forward:
pos.setX(pos.x() + speed);
break;
case command_back:
pos.setX(pos.x() - speed);
break;
case command_jump:
if (pos.y() == 0) {
pos.setY(pos.y() - speed);
}
break;
default:
break;
}
next_command = command_none;
animation->stop();
animation->setEndValue(pos - QPointF(0, object->boundingRect().height()));
animation->start();
}