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();
}
Related
I know this question is somehow hard to describe, so a picture may help, as following:
I'm developing application in qt-5.15.x. Say I have a window or widget or something else which can layout two columns of widgets(customized, call it rectangle here), I want to:
drag and drop one item up or down within same column by mouse
connect two widgets from two different columns with a customized curve path by mouse
all elements(rectangle and curve item) have mouse events: move, hover, click and key event
the contents of window or widget can scroll vertically due to more and more items will be added
Before this, I have implemented bazier curve, graphicsview with customized widgets and connecting these widgets with bazier curve, but all these widgets don't lay on line(horizontally or vertically).
So my question is not about how to implement it in detail, but just a guide - what widget , layout or event something else in qt I can use, or which document I can refer to. I have searched a lot, but with no results.
OPs requirement can be achieved by simply deriving a widget from QWidget and overriding the paintEvent().
Beside of that, the derived widget still can contain children and layout managers, so that both can be combined.
Sample code testQWidgetPaintOverChildren.cc to demonstrate:
#include <QtWidgets>
class Canvas: public QWidget {
private:
using Link = std::pair<int, int>;
std::vector<Link> _links;
public:
void addLink(int from, int to)
{
_links.emplace_back(from, to);
update();
}
protected:
virtual void paintEvent(QPaintEvent *pQEvent) override;
};
void Canvas::paintEvent(QPaintEvent* pQEvent)
{
QWidget::paintEvent(pQEvent);
const int lenTan = 64;
QPainter qPainter(this);
auto drawCurve = [&](const QPoint& qPosFrom, const QPoint& qPosTo) {
QPainterPath qPath;
qPath.moveTo(qPosFrom);
qPath.cubicTo(qPosFrom + QPoint(lenTan, 0), qPosTo - QPoint(lenTan, 0), qPosTo);
qPainter.drawPath(qPath);
};
const QObjectList& pQChildren = children();
for (const Link& link : _links) {
const int from = link.first;
if (from >= pQChildren.size()) continue; // invalid from index
const QWidget* pQWidgetFrom
= dynamic_cast<const QWidget*>(pQChildren[from]);
if (!pQWidgetFrom) continue; // shouldn't happen
const int to = link.second;
if (to >= pQChildren.size()) continue; // invalid to index
const QWidget* pQWidgetTo
= dynamic_cast<const QWidget*>(pQChildren[to]);
if (!pQWidgetTo) continue; // shouldn't happen
const QPoint qPosFrom
= pQWidgetFrom->pos() + QPoint(3 * pQWidgetFrom->width() / 4, pQWidgetFrom->height() / 2);
const QPoint qPosTo
= pQWidgetTo->pos() + QPoint(1 * pQWidgetTo->width() / 4, pQWidgetTo->height() / 2);
//qPainter.drawLine(qPosFrom, qPosTo);
drawCurve(qPosFrom, qPosTo);
}
}
int main(int argc, char** argv)
{
qDebug() << "Qt Version:" << QT_VERSION_STR;
QApplication app(argc, argv);
// setup GUI
Canvas qCanvas;
qCanvas.setWindowTitle("Test Draw Over Children");
qCanvas.resize(320, 240);
QGridLayout qGrid;
qGrid.setSpacing(16);
#define ITEM(ROW, COL) \
QLabel qLbl##ROW##COL("txt "#ROW", "#COL); \
qLbl##ROW##COL.setAlignment(Qt::AlignCenter); \
qLbl##ROW##COL.setFrameStyle(QFrame::Box | QFrame::Plain); \
qGrid.addWidget(&qLbl##ROW##COL, ROW - 1, COL - 1)
ITEM(1, 1); ITEM(1, 2);
ITEM(2, 1); ITEM(2, 2);
ITEM(3, 1); ITEM(3, 2);
ITEM(4, 1); ITEM(4, 2);
qCanvas.setLayout(&qGrid);
qCanvas.addLink(1, 8);
qCanvas.addLink(5, 2);
qCanvas.show();
// runtime loop
return app.exec();
}
Output:
Qt Version: 5.15.1
The drawback of this first attempt is that the links are drawn under the children. In the case of the QLabel, this isn't visible as QLabels are mostly transparent. Replacing them by QPushButtons makes this obvious.
This can be solved by using two widgets, one as container for children, the other for drawing the links, and ensuring that the second widget has the same position and size than the first.
Improved sample code testQWidgetPaintOverChildren.cc:
#include <QtWidgets>
class Widget: public QWidget {
private:
using Link = std::pair<int, int>;
struct Canvas: public QWidget {
std::vector<Link> links;
using QWidget::QWidget;
virtual void paintEvent(QPaintEvent *pQEvent) override;
} _canvas;
public:
explicit Widget(QWidget* pQParent = nullptr):
QWidget(pQParent),
_canvas(this)
{ }
public:
void addLink(int from, int to)
{
_canvas.links.emplace_back(from, to);
_canvas.update();
_canvas.setParent(nullptr);
_canvas.setParent(this);
}
protected:
virtual void resizeEvent(QResizeEvent* pQEvent) override
{
QWidget::resizeEvent(pQEvent);
_canvas.resize(size());
}
};
void Widget::Canvas::paintEvent(QPaintEvent* pQEvent)
{
QWidget::paintEvent(pQEvent);
const int lenTan = 64;
QPainter qPainter(this);
auto drawCurve = [&](const QPoint& qPosFrom, const QPoint& qPosTo) {
QPainterPath qPath;
qPath.moveTo(qPosFrom);
qPath.cubicTo(qPosFrom + QPoint(lenTan, 0), qPosTo - QPoint(lenTan, 0), qPosTo);
qPainter.drawPath(qPath);
};
const QObjectList& pQChildren = parent()->children();
for (const Link& link : links) {
const int from = link.first;
if (from >= pQChildren.size()) continue; // invalid from index
const QWidget* pQWidgetFrom
= dynamic_cast<const QWidget*>(pQChildren[from]);
if (!pQWidgetFrom) continue; // shouldn't happen
const int to = link.second;
if (to >= pQChildren.size()) continue; // invalid to index
const QWidget* pQWidgetTo
= dynamic_cast<const QWidget*>(pQChildren[to]);
if (!pQWidgetTo) continue; // shouldn't happen
const QPoint qPosFrom
= pQWidgetFrom->pos() + QPoint(3 * pQWidgetFrom->width() / 4, pQWidgetFrom->height() / 2);
const QPoint qPosTo
= pQWidgetTo->pos() + QPoint(1 * pQWidgetTo->width() / 4, pQWidgetTo->height() / 2);
//qPainter.drawLine(qPosFrom, qPosTo);
drawCurve(qPosFrom, qPosTo);
}
}
int main(int argc, char** argv)
{
qDebug() << "Qt Version:" << QT_VERSION_STR;
QApplication app(argc, argv);
// setup GUI
Widget qWinMain;
qWinMain.setWindowTitle("Test Draw Over Children");
qWinMain.resize(320, 240);
QGridLayout qGrid;
qGrid.setSpacing(16);
#define ITEM(ROW, COL) \
QPushButton qBtn##ROW##COL("txt "#ROW", "#COL); \
qGrid.addWidget(&qBtn##ROW##COL, ROW - 1, COL - 1)
ITEM(1, 1); ITEM(1, 2);
ITEM(2, 1); ITEM(2, 2);
ITEM(3, 1); ITEM(3, 2);
ITEM(4, 1); ITEM(4, 2);
#undef ITEM
qWinMain.setLayout(&qGrid);
qWinMain.addLink(1, 8);
qWinMain.addLink(5, 2);
qWinMain.show();
// runtime loop
return app.exec();
}
Output:
Qt Version: 5.15.1
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'm trying to create a program in which you can connect points together with lines. I instantiate QGraphicsEllipseItem into a QGraphicsScene and I use HoverEnterEvent and HoverLeaveEvent to change the color and the size of the ellipses when the mouse is over them. To draw a temporary line between the point clicked and the mouse cursor I have to use MouseMoveEvent into the scene. However, when I do that, the HoverEvents of the items don't work anymore ! How can I use both MouseMoveEvent of the scene and HoverEvents of the items ?
void GraphicsScene::mousePressEvent(QGraphicsSceneMouseEvent *mouseEvent)
{
for (int i = 0; i < pointList.size(); ++i) {
if (pointList[i]->over == 1){
pointList[i]->press();
lineActivated=true;
tempLine.setLine(pointList[i]->x(),pointList[i]->y(),pointList[i]->x(),pointList[i]->y());
}
}
}
void GraphicsScene::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
if(lineActivated){
const QPointF pos = event->scenePos();
tempLine.setP2(pos);
}
}
void Point::hoverEnterEvent(QGraphicsSceneHoverEvent *event)
{
pen.setColor(Qt::green);
pen.setWidth(2);
this->setPen(pen);
over=true;
qDebug("enter");
update();
}
void Point::hoverLeaveEvent(QGraphicsSceneHoverEvent *event)
{
if(isClicked==false){
pen.setColor(Qt::lightGray);
pen.setWidth(1);
over=false;
this->setPen(pen);
qDebug("leave");
update();
}
}
By default QGraphicsScene::mouseMoveEvent sends the necessary information to handle the hover event of the items but override that method you eliminate that behavior. The solution is to call the parent method.
#include <QtWidgets>
class GraphicsScene: public QGraphicsScene{
public:
using QGraphicsScene::QGraphicsScene;
protected:
void mousePressEvent(QGraphicsSceneMouseEvent *event){
if(!m_lineitem){
m_lineitem = new QGraphicsLineItem;
addItem(m_lineitem);
}
QLineF l(event->scenePos(), event->scenePos());
m_lineitem->setLine(l);
QGraphicsScene::mousePressEvent(event);
}
void mouseMoveEvent(QGraphicsSceneMouseEvent *event){
if(m_lineitem){
QLineF l(m_lineitem->line().p1(), event->scenePos());
m_lineitem->setLine(l);
}
QGraphicsScene::mouseMoveEvent(event);
}
private:
QGraphicsLineItem *m_lineitem = nullptr;
};
class Point: public QGraphicsEllipseItem{
public:
Point(QGraphicsItem *parent=nullptr): QGraphicsEllipseItem(parent){
setRect(QRectF(-5, -5, 10, 10));
QPen pen;
pen.setColor(Qt::lightGray);
pen.setWidth(1);
setPen(pen);
setAcceptHoverEvents(true);
}
protected:
void hoverEnterEvent(QGraphicsSceneHoverEvent *event){
QPen pen;
pen.setColor(Qt::green);
pen.setWidth(2);
setPen(pen);
QGraphicsEllipseItem::hoverEnterEvent(event);
}
void hoverLeaveEvent(QGraphicsSceneHoverEvent *event){
QPen pen;
pen.setColor(Qt::lightGray);
pen.setWidth(1);
setPen(pen);
QGraphicsEllipseItem::hoverLeaveEvent(event);
}
};
int main(int argc, char *argv[]){
QApplication a(argc, argv);
GraphicsScene *scene = new GraphicsScene;
QGraphicsView w(scene);
w.setRenderHint(QPainter::Antialiasing, true);
w.fitInView(QRectF(0, 0, 100, 100), Qt::KeepAspectRatio);
for(const QPointF & p: {QPointF(10.0, 10.0), QPointF(90.0, 20.0), QPointF(30.0, 40.0)}){
Point *it = new Point();
scene->addItem(it);
it->setPos(p);
}
w.resize(640, 480);
w.show();
return a.exec();
}
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
}
I have implemented custom QQuickItem which is exposed to QML. Now it becomes more useful item in all platforms but due some reasons in Windows we are not using QML it's pure QT Application.
But I am not able to instantiate custom QQuickItem in Qt, could some one help me out of this?
Here is the code:
customItem.h
#include <QQuickItem>
#include <QQuickWindow>
class CustomItem : public QQuickItem
{
Q_OBJECT
public:
explicit CustomItem(QQuickItem *parent = 0);
signals:
public slots:
void paint();
private slots:
void handleWindowChanged(QQuickWindow * win);
};
customItem.cpp:
#include "customItem.h"
CustomItem::CustomItem(QQuickItem *parent) :
QQuickItem(parent)
{
connect(this, &CustomItem::windowChanged, this, &CustomItem::handleWindowChanged);
}
void CustomItem::paint()
{
QQuickWindow * win = window();
qreal ratio = win->devicePixelRatio();
int w = int(ratio * win->width());
int h = int(ratio * win->height());
glViewport(0, 0, w, h);
glDisable(GL_DEPTH_TEST);
glClearColor(0, 1, 0, 1);
glClear(GL_COLOR_BUFFER_BIT);
}
void CustomItem::handleWindowChanged(QQuickWindow * win)
{
if (win) {
connect(win, &QQuickWindow::beforeRendering, this, &CustomItem::paint, Qt::DirectConnection);
win->setClearBeforeRendering(false);
}
}
main.cpp:
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QQuickView *view = new QQuickView ();
QWidget *container = QWidget::createWindowContainer(view);
CustomItem *customitem = new CustomItem();///how can i set it view
container->show();
return app.exec();
}
Issue:
Not able to instantiate CustomItem