How to add curve path to connect flow widgets from two columns in qt? - qt

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

Related

How to connect a QRadioButton of a QTableWidget cell with another QTableWidget cell using an arrow?

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.

QListView max number of items in view

I need to calculate max number of items in current view of QListView.
I wrote code like this:
void MyListView::resizeEvent(QResizeEvent *event)
{
QListView::resizeEvent(event);
QFontMetrics fm (this->font());
int fontHeight = fm.lineSpacing();
QRect cr = contentsRect();
int windowHeight = cr.bottom() - cr.top();
int maxItemsCount = windowHeight / fontHeight;
qDebug()<<"max items in view: "<< maxItemsCount;
}
but calculated max number of items is is incorrect.
E.g. in case of my window height and font height I get 32 max items in view when in fact current view has 28 items. Perhaps someone can suggest something, how to calculate it properly?
My idea is to use QListView::indexAt() (inherited from QAbstractView) to obtain the row index for
the top-left corner
the bottom-left corner
of the list view viewport and determining the number of visible items by difference of them.
To check this out, I made an MCVE testQListViewNumVisibleItems.cc:
// Qt header:
#include <QtWidgets>
class ListWidget: public QListWidget {
public:
ListWidget(QWidget *pQParent = nullptr): QListWidget(pQParent) { }
virtual ~ListWidget() = default;
ListWidget(const ListWidget&) = delete;
ListWidget& operator=(const ListWidget&) = delete;
int getNumVisibleItems() const
{
const QSize size = viewport()->size();
const QModelIndex qMIdx0 = indexAt(QPoint(0, 0));
const QModelIndex qMIdx1 = indexAt(QPoint(0, size.height() - 1));
//qDebug() << "qMIdx0:" << qMIdx0 << "qMIdx1:" << qMIdx1;
return qMIdx0.isValid()
? (qMIdx1.isValid() ? qMIdx1.row() : count()) - qMIdx0.row()
: 0;
}
};
const int MaxItems = 20;
// main application
int main(int argc, char **argv)
{
qDebug() << "Qt Version:" << QT_VERSION_STR;
QApplication app(argc, argv);
// setup GUI
ListWidget qLst;
qLst.resize(200, 200);
qLst.show();
// timer to populate list view
using namespace std::chrono_literals;
QTimer qTimer;
qTimer.setInterval(1000ms);
// install signal handlers
int n = 0;
QObject::connect(&qTimer, &QTimer::timeout,
[&]() {
qLst.addItem(QString("item %0").arg(++n));
qDebug() << "Visible items:" << qLst.getNumVisibleItems();
if (n >= MaxItems) qTimer.stop();
});
// runtime loop
qTimer.start();
return app.exec();
}
and a CMakeLists.txt:
project(QListViewNumVisibleItems)
cmake_minimum_required(VERSION 3.10.0)
set_property(GLOBAL PROPERTY USE_FOLDERS ON)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
find_package(Qt5Widgets CONFIG REQUIRED)
include_directories("${CMAKE_SOURCE_DIR}")
add_executable(testQListViewNumVisibleItems testQListViewNumVisibleItems.cc)
target_link_libraries(testQListViewNumVisibleItems Qt5::Widgets)
built and tested in VS2017 on Windows 10:
After having implemented what came in my mind, I googled a bit to possibly see other approaches. (I admit I should've done before.)
Thereby I found the following possible duplicate:
SO: Simple way to get all visible items in the QListView
The accepted answer doesn't contain much more than the hint for indexAt and a link to a Qt-FAQ article:
How can i get hold of all of the visible items in my QListView?
In order to get hold of the visible items in a QListView http://doc.qt.io/qt-5/qlistview.html, then you can iterate over them using indexAt() http://doc.qt.io/qt-5/qlistview.html#indexAt. You can get hold of the first visible item using indexAt(QPoint(0, 0)), then in order to get the index at the next position then use visualRect() http://doc.qt.io/qt-5/qlistview.html#visualRect to find out what your next call to itemAt() should be. This position would be:
visualRect.y() + visualRect.height() + 1 effectively.
See the following example for an illustration:
#include <QtGui>
QList <QModelIndex>myList;
class ListView : public QListView
{
Q_OBJECT
public:
ListView()
{
QStringListModel *myModel = new QStringListModel(this);
QStringList list;
list << "a" << "b" <<"c" <<"d" <<"e" <<"f" <<"g" <<"h" <<"i" <<"j" <<"k";
myModel->setStringList(list);
setModel(myModel);
QTimer::singleShot(3000, this, SLOT(test1()));
}
public slots:
void test1()
{
QModelIndex firstIndex = indexAt(QPoint(0, 0));
if (firstIndex.isValid()) {
myList << firstIndex;
} while (viewport()->rect().contains(QPoint(0, visualRect(firstIndex).y() + visualRect(firstIndex).height() + 1 ))) {
firstIndex = indexAt(QPoint(0, visualRect(firstIndex).y() + visualRect(firstIndex).height() + 1 ));
myList << firstIndex;
}
qDebug() << myList.count() << "are visible";
}
};
#include "main.moc"
int main(int argc, char** argv)
{
QApplication app(argc, argv);
ListView window;
window.resize(100, 50);
window.show();
return app.exec();
}

QCharts Crop to Rectangle and Use Horizontal Scroll

am trying to implement a custom graph going off the QtCharts Callout example. I want to restrict the selection of the chart to a specific area and make it possible to scroll horizontally while still displaying the Axis Values.
the classes i am using are below
callout.cpp
callout.h
main.cpp
view.cpp
view.h
here is an example of what i mean
say i want the selection region point1 = (5,0) point2 = (15,8) and the region is a QRect(point1,point2)
All points in the graph should be rendered but I want to be able to scroll sideways and keep the y_axis in view.
One possible solution is to override the mousePressEvent and mouseMoveEvent methods to apply the scroll, and correct using the axes ranges if necessary:
#include <QtWidgets>
#include <QtCharts>
#include <algorithm>
QT_CHARTS_USE_NAMESPACE
class ChartView: public QChartView{
public:
using QChartView::QChartView;
void setRange(qreal xmin, qreal xmax, qreal ymin, qreal ymax){
if(!chart()) return;
if(QValueAxis *xaxis = qobject_cast<QValueAxis *>(chart()->axes(Qt::Horizontal).first())){
xaxis->setRange(xmin, xmax);
}
if(QValueAxis *yaxis = qobject_cast<QValueAxis *>(chart()->axes(Qt::Vertical).first())){
yaxis->setRange(ymin, ymax);
}
}
void setLimits(qreal min, qreal max, Qt::Orientation orientation){
m_limit_min = min;
m_limit_max = max;
m_orientation = orientation;
}
protected:
void mousePressEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton && chart())
m_lastMousePos = mapToScene(event->pos());
QGraphicsView::mousePressEvent(event);
}
void mouseMoveEvent(QMouseEvent *event)
{
if(event->buttons() & Qt::LeftButton && chart()){
QPointF newValue = mapToScene(event->pos());
QPointF delta = newValue - m_lastMousePos;
if(m_orientation == Qt::Horizontal)
chart()->scroll(-delta.x(), 0);
else
chart()->scroll(0, -delta.y());
if(QValueAxis * axis = qobject_cast<QValueAxis *>(chart()->axes(m_orientation).first()) ){
qreal deltaX = axis->max() - axis->min();
if(axis->min() < m_limit_min){
axis->setRange(m_limit_min, m_limit_min + deltaX);
}
else if(axis->max() > m_limit_max){
axis->setRange(m_limit_max - deltaX, m_limit_max);
}
}
m_lastMousePos = newValue;
}
QGraphicsView::mouseMoveEvent(event);
}
private:
QPointF m_lastMousePos;
qreal m_limit_min;
qreal m_limit_max;
Qt::Orientation m_orientation;
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
ChartView chartView;
chartView.setRenderHint(QPainter::Antialiasing);
chartView.resize(640, 480);
QLineSeries *series = new QLineSeries();
series->append(0, 6);
series->append(2, 4);
series->append(3, 8);
series->append(7, 4);
series->append(10, 5);
*series << QPointF(11, 1) << QPointF(13, 3) << QPointF(17, 6) << QPointF(18, 3) << QPointF(20, 2);
QChart *chart = chartView.chart();
chart->legend()->hide();
chart->addSeries(series);
chart->createDefaultAxes();
chartView.show();
chartView.setRange(5, 15, 0, 8);
chartView.setLimits(0, 20, Qt::Horizontal);
return a.exec();
}

How to perform swap of QGraphicsItem(s) in QGraphicsView?

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

Graphic item jumps to the end of path via QGraphicsItemAnimation without moving

I have a circle which I want to move smoothly on a path. The path class is like a horizontal U derived from the QPainterPath. when I start timer (QTimeLine object) the circle just jumps from the start of path to the end (start of upper U fork to the end of lower fork) with no smooth animation. Unfortunately, the QTimeLine::setLoopCount(int n) doesn't work too.
Do you have any idea about the reason?
// UPath(int forkLen, int forksDistance, QPointF startPoint)
UPath* uPath = new UPath(500, 60, QPointF(10, 10));
QList<QPointF> points = uPath->pathPoints(0.006); // returns the points of the path
// implemented by QPainterPath::pointAtPercent()
QGraphicsItem *ball = new QGraphicsEllipseItem(0, 0, 10, 10);
QTimeLine *timer = new QTimeLine(5000);
timer->setFrameRange(0, 100);
timer->setLoopCount(2); // doesn't work
QGraphicsItemAnimation *animation = new QGraphicsItemAnimation;
animation->setItem(ball);
animation->setTimeLine(timer);
for (int i = 0; i < points.count(); ++i)
animation->setPosAt(i/points.count(), points.at(i));
QGraphicsScene *scene = new QGraphicsScene();
scene->addItem(ball);
QGraphicsView *view = new QGraphicsView(scene);
view->setRenderHint(QPainter::Antialiasing);
view->show();
timer->start();
The QGraphicsAnimation class is deprecated. What you want is an adapter between a QPainterPath and the animation system. See below for a complete example.
Using painter paths for animations requires some extra smoothing (resampling) as there will be velocity changes along the path, and it won't look all that great. You may notice it when you run the code below. Painter paths are meant for painting, not for animating stuff.
The extent of this misbehavior will depend on the kind of path you're using, so it may end up working OK for the particular use case you have.
#include <QApplication>
#include <QAbstractAnimation>
#include <QPainterPath>
#include <QGraphicsScene>
#include <QGraphicsView>
#include <QGraphicsEllipseItem>
#include <QDebug>
class PathAnimation : public QAbstractAnimation {
Q_OBJECT
Q_PROPERTY(int duration READ duration WRITE setDuration)
QPainterPath m_path;
int m_duration;
QVector<QPointF> m_cache;
QGraphicsItem * m_target;
int m_hits, m_misses;
public:
PathAnimation(const QPainterPath & path, QObject * parent = 0) :
QAbstractAnimation(parent), m_path(path), m_duration(1000), m_cache(m_duration), m_target(0), m_hits(0), m_misses(0) {}
~PathAnimation() { qDebug() << m_hits << m_misses; }
int duration() const { return m_duration; }
void setDuration(int duration) {
if (duration == 0 || duration == m_duration) return;
m_duration = duration;
m_cache.clear();
m_cache.resize(m_duration);
}
void setTarget(QGraphicsItem * target) {
m_target = target;
}
void updateCurrentTime(int ms) {
QPointF point = m_cache.at(ms);
if (! point.isNull()) {
++ m_hits;
} else {
point = m_path.pointAtPercent(qreal(ms) / m_duration);
m_cache[ms] = point;
++ m_misses;
}
if (m_target) m_target->setPos(point);
}
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QGraphicsEllipseItem * item = new QGraphicsEllipseItem(-5, -5, 10, 10);
item->setPen(QPen(Qt::red, 2));
item->setBrush(Qt::lightGray);
QPainterPath path;
path.addEllipse(0, 0, 100, 100);
PathAnimation animation(path);
animation.setTarget(item);
QGraphicsScene scene;
scene.addItem(item);
QGraphicsView view(&scene);
view.setSceneRect(-50, -50, 200, 200);
animation.setLoopCount(-1);
animation.start();
view.show();
return a.exec();
}
#include "main.moc"

Resources