QCharts Crop to Rectangle and Use Horizontal Scroll - qt

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

Related

How to add curve path to connect flow widgets from two columns in 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

How to align Graphics items in Graphic scene based on first selected item?

I have Graphic scene in that i have to alight right,left,top or bottom based on first selected item(Reference Item). i searched i got some code but in this its aligning to scene right position. I have to align items based on first selected item. how can i do this?
void GraphicScene::ItemsRightAlign()
{
if (selectedItems().isEmpty())
return;
QRectF refRect = selectedItems().first()->boundingRect();
QList<QGraphicsItem*> sel =selectedItems(); // for example
foreach(QGraphicsItem* selItem, sel)
{
qreal dx = 0, dy = 0;
QRectF itemRect = selItem->mapToScene(selItem->boundingRect()).boundingRect();
//if(align_right)
dx = refRect.right() - itemRect.right();
qDebug() << "item pos "<< dx << dy << selItem->mapToScene(selItem->boundingRect()).boundingRect() ;
selItem->moveBy(dx, dy);
}
}
For more details
Output should be like this output
The resolution method is to map the point that determines the right, left, up, down to the scene of the first item and the other items obtaining the difference that must be compensated.
graphicsscene.h
#ifndef GRAPHICSSCENE_H
#define GRAPHICSSCENE_H
#include <QGraphicsScene>
#include <QGraphicsItem>
class GraphicsScene: public QGraphicsScene{
Q_OBJECT
public:
GraphicsScene(QObject *parent=nullptr);
void moveSelecteds(Qt::Alignment aligment);
private slots:
void onSelectionChanged();
private:
void move(QGraphicsItem *ref, QList<QGraphicsItem *> others, Qt::Alignment aligment);
QGraphicsItem *mRef;
};
#endif // GRAPHICSSCENE_H
graphicsscene.cpp
#include "graphicsscene.h"
GraphicsScene::GraphicsScene(QObject *parent):
QGraphicsScene(parent),
mRef(nullptr)
{
connect(this, &GraphicsScene::selectionChanged, this, &GraphicsScene::onSelectionChanged);
}
void GraphicsScene::moveSelecteds(Qt::Alignment aligment){
QList<QGraphicsItem *> its= selectedItems();
if(its.size() < 2)
return;
if(!its.removeOne(mRef))
return;
move(mRef, its, aligment);
}
void GraphicsScene::onSelectionChanged(){
if(selectedItems().isEmpty()){
mRef = nullptr;
}
else if(selectedItems().size() == 1){
mRef = selectedItems().first();
}
}
void GraphicsScene::move(QGraphicsItem *ref, QList<QGraphicsItem *> others, Qt::Alignment aligment){
QPointF p;
switch (aligment) {
case Qt::AlignLeft:
p = QPointF(ref->mapToScene(ref->boundingRect().topLeft()).x(), 0);
break;
case Qt::AlignRight:
p = QPointF(ref->mapToScene(ref->boundingRect().topRight()).x(), 0);
break;
case Qt::AlignTop:
p = QPointF(0, ref->mapToScene(ref->boundingRect().topLeft()).y());
break;
case Qt::AlignBottom:
p = QPointF(0, ref->mapToScene(ref->boundingRect().bottomLeft()).y());
break;
}
for(QGraphicsItem *o: others){
QPointF delta;
switch (aligment) {
case Qt::AlignLeft:{
delta = p - QPointF(o->mapToScene(o->boundingRect().topLeft()).x(), 0);
break;
}
case Qt::AlignRight:{
delta = p - QPointF(o->mapToScene(o->boundingRect().topRight()).x(), 0);
break;
}
case Qt::AlignTop:{
delta = p - QPointF(0, o->mapToScene(o->boundingRect().topLeft()).y());
break;
}
case Qt::AlignBottom:{
delta = p - QPointF(0, o->mapToScene(o->boundingRect().bottomLeft()).y());
break;
}
}
o->moveBy(delta.x(), delta.y());
}
}
In this example you can use the up, down, left, right keys to move the items.
main.cpp
#include "graphicsscene.h"
#include <QApplication>
#include <QGraphicsView>
#include <QGraphicsRectItem>
#include <QShortcut>
#include <random>
static void create_items(QGraphicsScene & scene){
std::default_random_engine generator;
std::uniform_int_distribution<int> dist_size(30, 40);
std::uniform_int_distribution<int> dist_pos(-50, 50);
for(const QString & colorname : {"red", "green", "blue", "gray", "orange"}){
QRectF r(QPointF(dist_pos(generator), dist_pos(generator)),
QSizeF(dist_size(generator), dist_size(generator)));
auto item = new QGraphicsRectItem(r);
item->setPos(QPointF(dist_pos(generator), dist_pos(generator)));
item->setBrush(QColor(colorname));
item->setFlag(QGraphicsItem::ItemIsSelectable);
scene.addItem(item);
}
}
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
GraphicsScene scene;
create_items(scene);
QGraphicsView view(&scene);
const QList<QPair<Qt::Key, Qt::Alignment>> k_a {
{Qt::Key_Up, Qt::AlignTop},
{Qt::Key_Down, Qt::AlignBottom},
{Qt::Key_Left, Qt::AlignLeft},
{Qt::Key_Right, Qt::AlignRight}
};
for(const QPair<Qt::Key, Qt::Alignment> & p : k_a){
QShortcut *shorcut = new QShortcut(p.first, &view);
QObject::connect(shorcut, &QShortcut::activated, std::bind(&GraphicsScene::moveSelecteds, &scene, p.second));
}
view.resize(640, 480);
view.show();
return a.exec();
}
The complete example can be found in the following link.
Replace topLeft.x -- for Right align with topRight.x ,For Top Align replace topLeft.y and dx with dy,For Bottom Align replace bottomLeft.y and dx with dy
void GraphicScene::ItemsLeftAlign
{
if (selectedItems().isEmpty())
return;
QGraphicsItem *FirstSelItem = selectedItems().first();
QList<QGraphicsItem*> sel =selectedItems(); // for example
foreach(QGraphicsItem* selItem, sel)
{
qreal dx = 0, dy = 0;
dx = (FirstSelItem->mapToScene(FirstSelItem->boundingRect().topLeft()).x()) -
(selItem->mapToScene(selItem->boundingRect().topLeft()).x());
selItem->moveBy(dx, dy);
}
}

Snapping in grid using qt

I have implemented grid in graphicsView using drawBackgroud method. Now I also want to add snap to the grid. By snap I mean that with mouse, you can't have point other than grid points. My code to grid drawn is as follows:
void CadGraphicsScene::drawBackground(QPainter *painter, const QRectF &rect)
{
const int gridSize = 50;
const int realLeft = static_cast<int>(std::floor(rect.left()));
const int realRight = static_cast<int>(std::ceil(rect.right()));
const int realTop = static_cast<int>(std::floor(rect.top()));
const int realBottom = static_cast<int>(std::ceil(rect.bottom()));
// Draw grid.
const int firstLeftGridLine = realLeft - (realLeft % gridSize);
const int firstTopGridLine = realTop - (realTop % gridSize);
QVarLengthArray<QLine, 100> lines;
for (qreal x = firstLeftGridLine; x <= realRight; x += gridSize)
lines.append(QLine(x, realTop, x, realBottom));
for (qreal y = firstTopGridLine; y <= realBottom; y += gridSize)
lines.append(QLine(realLeft, y, realRight, y));
painter->setPen(QPen(QColor(220, 220, 220), 0.0));
painter->drawLines(lines.data(), lines.size());
// Draw axes.
painter->setPen(QPen(Qt::lightGray, 0.0));
painter->drawLine(0, realTop, 0, realBottom);
painter->drawLine(realLeft, 0, realRight, 0);
}
Please help me solve the problem and complete the task.
I tried to do it using itemChange method but nothing happened:
My code to it is as follows:
snap.cpp
#include "snap.h"
#include <QApplication>
Snap::Snap(const QRect& rect, QGraphicsItem* parent,
QGraphicsScene* scene):
QGraphicsRectItem(QRectF())
{
setFlags(QGraphicsItem::ItemIsSelectable |
QGraphicsItem::ItemIsMovable |
QGraphicsItem::ItemSendsGeometryChanges);
}
void Snap::mousePressEvent(QGraphicsSceneMouseEvent *event){
offset = pos() - computeTopLeftGridPoint(pos());
QGraphicsRectItem::mousePressEvent(event);
}
QVariant Snap::itemChange(GraphicsItemChange change,
const QVariant &value)
{
if (change == ItemPositionChange && scene()) {
QPointF newPos = value.toPointF();
if(QApplication::mouseButtons() == Qt::LeftButton &&
qobject_cast<CadGraphicsScene*> (scene())){
QPointF closestPoint = computeTopLeftGridPoint(newPos);
return closestPoint+=offset;
}
else
return newPos;
}
else
return QGraphicsItem::itemChange(change, value);
}
QPointF Snap::computeTopLeftGridPoint(const QPointF& pointP){
CadGraphicsScene* customScene = qobject_cast<CadGraphicsScene*> (scene());
int gridSize = customScene->getGridSize();
qreal xV = floor(pointP.x()/gridSize)*gridSize;
qreal yV = floor(pointP.y()/gridSize)*gridSize;
return QPointF(xV, yV);
}
snap.h
#ifndef SNAP_H
#define SNAP_H
#include <QGraphicsRectItem>
#include "cadgraphicsscene.h"
class Snap : public QGraphicsRectItem
{
public:
Snap(const QRect& rect, QGraphicsItem* parent,
QGraphicsScene* scene);
protected:
void mousePressEvent(QGraphicsSceneMouseEvent *event);
QVariant itemChange(GraphicsItemChange change,
const QVariant &value);
private:
QPointF offset;
QPointF computeTopLeftGridPoint(const QPointF &pointP);
};
#endif // SNAP_H
Please help me out to snap to grid.

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"

Finding the point of intersection between a line and a QPainterPath

I'm trying to determine the point where a hitscan projectile's path (basically a line, but I've represented it as a QPainterPath in my example) intersects with an item in my scene. I am not sure if there is a way to find this point using the functions provided by QPainterPath, QLineF, etc. The code below illustrates what I'm trying to do:
#include <QtWidgets>
bool hit(const QPainterPath &projectilePath, QGraphicsScene *scene, QPointF &hitPos)
{
const QList<QGraphicsItem *> itemsInPath = scene->items(projectilePath, Qt::IntersectsItemBoundingRect);
if (!itemsInPath.isEmpty()) {
const QPointF projectileStartPos = projectilePath.elementAt(0);
float shortestDistance = std::numeric_limits<float>::max();
QGraphicsItem *closest = 0;
foreach (QGraphicsItem *item, itemsInPath) {
QPointF distanceAsPoint = item->pos() - projectileStartPos;
float distance = abs(distanceAsPoint.x() + distanceAsPoint.y());
if (distance < shortestDistance) {
shortestDistance = distance;
closest = item;
}
}
QPainterPath targetShape = closest->mapToScene(closest->shape());
// hitPos = /* the point at which projectilePath hits targetShape */
hitPos = closest->pos(); // incorrect; always gives top left
qDebug() << projectilePath.intersects(targetShape); // true
qDebug() << projectilePath.intersected(targetShape); // QPainterPath: Element count=0
// To show that they do actually intersect..
QPen p1(Qt::green);
p1.setWidth(2);
QPen p2(Qt::blue);
p2.setWidth(2);
scene->addPath(projectilePath, p1);
scene->addPath(targetShape, p2);
return true;
}
return false;
}
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QGraphicsView view;
view.setViewportUpdateMode(QGraphicsView::FullViewportUpdate);
QGraphicsScene *scene = new QGraphicsScene;
view.setScene(scene);
view.setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
view.setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
QGraphicsItem *target = scene->addRect(0, 0, 25, 25);
target->setTransformOriginPoint(QPointF(12.5, 12.5));
target->setRotation(35);
target->setPos(100, 100);
QPainterPath projectilePath;
projectilePath.moveTo(200, 200);
projectilePath.lineTo(0, 0);
projectilePath.lineTo(200, 200);
QPointF hitPos;
if (hit(projectilePath, scene, hitPos)) {
scene->addEllipse(hitPos.x() - 2, hitPos.y() - 2, 4, 4, QPen(Qt::red));
}
scene->addPath(projectilePath, QPen(Qt::DashLine));
scene->addText("start")->setPos(180, 150);
scene->addText("end")->setPos(20, 0);
view.show();
return app.exec();
}
projectilePath.intersects(targetShape) returns true, but projectilePath.intersected(targetShape) returns an empty path.
Is there a way to achieve this?
As the answer to Intersection point of QPainterPath and line (find QPainterPath y by x) points out, QPainterPath::intersected() only accounts for paths which have fill areas. The rectangular path trick which is also mentioned there can be implemented like this:
#include <QtWidgets>
/*!
Returns the closest element (position) in \a sourcePath to \a target,
using \l{QPoint::manhattanLength()} to determine the distances.
*/
QPointF closestPointTo(const QPointF &target, const QPainterPath &sourcePath)
{
Q_ASSERT(!sourcePath.isEmpty());
QPointF shortestDistance = sourcePath.elementAt(0) - target;
qreal shortestLength = shortestDistance.manhattanLength();
for (int i = 1; i < sourcePath.elementCount(); ++i) {
const QPointF distance(sourcePath.elementAt(i) - target);
const qreal length = distance.manhattanLength();
if (length < shortestLength) {
shortestDistance = sourcePath.elementAt(i);
shortestLength = length;
}
}
return shortestDistance;
}
/*!
Returns \c true if \a projectilePath intersects with any items in \a scene,
setting \a hitPos to the position of the intersection.
*/
bool hit(const QPainterPath &projectilePath, QGraphicsScene *scene, QPointF &hitPos)
{
const QList<QGraphicsItem *> itemsInPath = scene->items(projectilePath, Qt::IntersectsItemBoundingRect);
if (!itemsInPath.isEmpty()) {
const QPointF projectileStartPos = projectilePath.elementAt(0);
float shortestDistance = std::numeric_limits<float>::max();
QGraphicsItem *closest = 0;
foreach (QGraphicsItem *item, itemsInPath) {
QPointF distanceAsPoint = item->pos() - projectileStartPos;
float distance = abs(distanceAsPoint.x() + distanceAsPoint.y());
if (distance < shortestDistance) {
shortestDistance = distance;
closest = item;
}
}
QPainterPath targetShape = closest->mapToScene(closest->shape());
// QLineF has normalVector(), which is useful for extending our path to a rectangle.
// The path needs to be a rectangle, as QPainterPath::intersected() only accounts
// for intersections between fill areas, which projectilePath doesn't have.
QLineF pathAsLine(projectileStartPos, projectilePath.elementAt(1));
// Extend the first point in the path out by 1 pixel.
QLineF startEdge = pathAsLine.normalVector();
startEdge.setLength(1);
// Swap the points in the line so the normal vector is at the other end of the line.
pathAsLine.setPoints(pathAsLine.p2(), pathAsLine.p1());
QLineF endEdge = pathAsLine.normalVector();
// The end point is currently pointing the wrong way; move it to face the same
// direction as startEdge.
endEdge.setLength(-1);
// Now we can create a rectangle from our edges.
QPainterPath rectPath(startEdge.p1());
rectPath.lineTo(startEdge.p2());
rectPath.lineTo(endEdge.p2());
rectPath.lineTo(endEdge.p1());
rectPath.lineTo(startEdge.p1());
// Visualize the rectangle that we created.
scene->addPath(rectPath, QPen(QBrush(Qt::blue), 2));
// Visualize the intersection of the rectangle with the item.
scene->addPath(targetShape.intersected(rectPath), QPen(QBrush(Qt::cyan), 2));
// The hit position will be the element (point) of the rectangle that is the
// closest to where the projectile was fired from.
hitPos = closestPointTo(projectileStartPos, targetShape.intersected(rectPath));
return true;
}
return false;
}
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QGraphicsView view;
QGraphicsScene *scene = new QGraphicsScene;
view.setScene(scene);
view.setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
view.setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
QGraphicsItem *target = scene->addRect(0, 0, 25, 25);
target->setTransformOriginPoint(QPointF(12.5, 12.5));
target->setRotation(35);
target->setPos(100, 100);
QPainterPath projectilePath;
projectilePath.moveTo(200, 200);
projectilePath.lineTo(0, 0);
projectilePath.lineTo(200, 200);
QPointF hitPos;
if (hit(projectilePath, scene, hitPos)) {
scene->addEllipse(hitPos.x() - 2, hitPos.y() - 2, 4, 4, QPen(Qt::red));
}
scene->addPath(projectilePath, QPen(Qt::DashLine));
scene->addText("start")->setPos(180, 150);
scene->addText("end")->setPos(20, 0);
view.show();
return app.exec();
}
This has pretty good precision (± 1 pixel, since QLineF::length() is an integer), but there might be a neater way to achieve the same thing.
Just for the record (and if someone else steps here). The above answer is excellent. There's just a little bug in the closestPoint function that may happens if the first point is already the closest one. It should return elementAt(0) instead of elementAt(0) - target.
Here is the fixed function:
QPointF closestPointTo(const QPointF &target, const QPainterPath &sourcePath)
{
Q_ASSERT(!sourcePath.isEmpty());
QPointF shortestDistance;
qreal shortestLength = std::numeric_limits<int>::max();
for (int i = 0; i < sourcePath.elementCount(); ++i) {
const QPointF distance(sourcePath.elementAt(i) - target);
const qreal length = distance.manhattanLength();
if (length < shortestLength) {
shortestDistance = sourcePath.elementAt(i);
shortestLength = length;
}
}
return shortestDistance;
}

Resources