Reputation: 5882
I have my own derived class of type QGraphicsLineItem where I override paint() in order to render it as an arrow.
My test line is 160, 130, 260, 230
And my paint() implementation:
void MyQGraphicsLineItem::paint( QPainter* aPainter, const QStyleOptionGraphicsItem* aOption, QWidget* aWidget /*= nullptr*/ )
{
Q_UNUSED( aWidget );
aPainter->setClipRect( aOption->exposedRect );
// Get the line and its angle
QLineF cLine = line();
const qreal cLineAngle = cLine.angle();
// Create two copies of the line
QLineF head1 = cLine;
QLineF head2 = cLine;
// Shorten each line and set its angle relative to the main lines angle
// this gives up the "arrow head" lines
head1.setLength( 12 );
head1.setAngle( cLineAngle+-32 );
head2.setLength( 12 );
head2.setAngle( cLineAngle+32 );
// Draw shaft
aPainter->setPen( QPen( Qt::black, 1, Qt::SolidLine ) );
aPainter->drawLine( cLine );
// Draw arrow head
aPainter->setPen( QPen( Qt::red, 1, Qt::SolidLine ) );
aPainter->drawLine( head1 );
aPainter->setPen( QPen( Qt::magenta, 1, Qt::SolidLine ) );
aPainter->drawLine( head2 );
}
This draws an arrow which looks like this:
What I would like to do is be able to calculate the "outline" of this item, such that I can draw a filled QPolygon from the data.
I can't use any shortcuts such as drawing two lines with different pen widths because I want the outline to be an animated "dashed" line (aka marching ants).
I'm sure this is simple to calculate but my maths skills are very bad - I attempt to create a parallel line by doing the following:
Hopefully someone can put me on the right track to creating a thick QPolygonF (or anything else if it makes sense) from this line which can then have an outline and fill set for painting.
Also I plan to have 1000's of these in my scene so ideally I'd also want a solution which won't take too much execution time or has a simple way of being optimized.
This image here is what I'm trying to achieve - imagine the red line is a qt dashed line rather than my very bad mspaint attempt at drawing it!
Upvotes: 0
Views: 1635
Reputation: 5882
I almost forgot about this question, here was my PyQt solution, I'm not sure if there is any way its performance can be improved.
class ArrowItem(QGraphicsLineItem):
def __init__(self, x, y , w, h, parent = None):
super(ArrowItem, self).__init__( x, y, w, h, parent)
self.init()
def paint(self, painter, option, widget):
painter.setClipRect( option.exposedRect )
painter.setBrush( Qt.yellow )
if self.isSelected():
p = QPen( Qt.red, 2, Qt.DashLine )
painter.setPen( p )
else:
p = QPen( Qt.black, 2, Qt.SolidLine )
p.setJoinStyle( Qt.RoundJoin )
painter.setPen( p )
painter.drawPath( self.shape() )
def shape(self):
# Calc arrow head lines based on the angle of the current line
cLine = self.line()
kArrowHeadLength = 13
kArrowHeadAngle = 32
cLineAngle = cLine.angle()
head1 = QLineF(cLine)
head2 = QLineF(cLine)
head1.setLength( kArrowHeadLength )
head1.setAngle( cLineAngle+-kArrowHeadAngle )
head2.setLength( kArrowHeadLength )
head2.setAngle( cLineAngle+kArrowHeadAngle )
# Create paths for each section of the arrow
mainLine = QPainterPath()
mainLine.moveTo( cLine.p2() )
mainLine.lineTo( cLine.p1() )
headLine1 = QPainterPath()
headLine1.moveTo( cLine.p1() )
headLine1.lineTo( head1.p2() )
headLine2 = QPainterPath()
headLine2.moveTo( cLine.p1() )
headLine2.lineTo( head2.p2() )
stroker = QPainterPathStroker()
stroker.setWidth( 4 )
# Join them together
stroke = stroker.createStroke( mainLine )
stroke.addPath( stroker.createStroke( headLine1 ) )
stroke.addPath( stroker.createStroke( headLine2 ) )
return stroke.simplified()
def boundingRect(self):
pPath = self.shape()
bRect = pPath.controlPointRect()
adjusted = QRectF( bRect.x()-1, bRect.y()-1, bRect.width()+2, bRect.height()+2 )
return adjusted
.. and of course set the item to be movable/selectable.
And so you can see the required class to get the "outlines" is QPainterPathStroker.
http://doc.qt.io/qt-5/qpainterpathstroker.html#details
Upvotes: 0
Reputation: 383
This solution works even if the arrow is moved and rotated in the scene later on:
arrow.h
#ifndef ARROW_H
#define ARROW_H
#include <QGraphicsLineItem>
#include <QObject>
#include <QtCore/qmath.h>
class Arrow : public QGraphicsLineItem, public QObject
{
public:
Arrow(qreal x1, qreal y1, qreal x2, qreal y2, QGraphicsItem* parent = 0);
virtual ~Arrow();
QPointF objectEndPoint1();
QPointF objectEndPoint2();
void setObjectEndPoint1(qreal x1, qreal y1);
void setObjectEndPoint2(qreal x2, qreal y2);
protected:
void paint(QPainter*, const QStyleOptionGraphicsItem*, QWidget*);
void timerEvent(QTimerEvent* event);
private:
inline qreal pi() { return (qAtan(1.0)*4.0); }
inline qreal radians(qreal degrees) { return (degrees*pi()/180.0); }
void createArrow(qreal penWidth);
QPainterPath arrowPath;
QPainterPath strokePath;
QPainterPath fillPath;
int timerID_Anim;
int animFrame;
qreal animLength;
QVector<qreal> dashPattern;
};
#endif
arrow.cpp
#include "arrow.h"
#include <QPen>
#include <QPainter>
#include <QTimerEvent>
Arrow::Arrow(qreal x1, qreal y1, qreal x2, qreal y2, QGraphicsItem* parent) : QGraphicsLineItem(0, 0, x2, y2, parent)
{
setFlag(QGraphicsItem::ItemIsSelectable, true);
setObjectEndPoint1(x1, y1);
setObjectEndPoint2(x2, y2);
qreal dashLength = 3;
qreal dashSpace = 3;
animLength = dashLength + dashSpace;
dashPattern << dashLength << dashSpace;
createArrow(1.0);
animFrame = 0;
timerID_Anim = startTimer(100);
}
Arrow::~Arrow()
{
}
void Arrow::timerEvent(QTimerEvent* event)
{
if(event->timerId() == timerID_Anim)
{
animFrame++;
if(animFrame >= animLength) animFrame = 0;
}
update(); //This forces a repaint, even if the mouse isn't moving
}
void Arrow::createArrow(qreal penWidth)
{
QPen arrowPen = pen();
arrowPen.setWidthF(penWidth);
arrowPen.setDashPattern(dashPattern);
setPen(arrowPen);
QPointF p1 = line().p1();
QPointF p2 = line().p2();
qreal angle = line().angle();
qreal arrowHeadAngle = 32.0;
qreal length = line().length();
qreal arrowHeadLength = length/10.0;
QLineF arrowLine1(p1, p2);
QLineF arrowLine2(p1, p2);
arrowLine1.setAngle(angle + arrowHeadAngle);
arrowLine2.setAngle(angle - arrowHeadAngle);
arrowLine1.setLength(arrowHeadLength);
arrowLine2.setLength(arrowHeadLength);
QPainterPath linePath;
linePath.moveTo(p1);
linePath.lineTo(p2);
QPainterPath arrowheadPath;
arrowheadPath.moveTo(arrowLine1.p2());
arrowheadPath.lineTo(p1);
arrowheadPath.lineTo(arrowLine2.p2());
arrowheadPath.lineTo(p1);
arrowheadPath.lineTo(arrowLine1.p2());
arrowPath = QPainterPath();
arrowPath.addPath(linePath);
arrowPath.addPath(arrowheadPath);
}
void Arrow::paint(QPainter* painter, const QStyleOptionGraphicsItem* /*option*/, QWidget* /*widget*/)
{
QPen paintPen = pen();
QPainterPathStroker stroker;
stroker.setWidth(paintPen.widthF());
stroker.setCapStyle(Qt::FlatCap);
stroker.setJoinStyle(Qt::MiterJoin);
strokePath = stroker.createStroke(arrowPath);
strokePath = strokePath.simplified();
stroker.setDashOffset(animFrame);
stroker.setDashPattern(dashPattern);
fillPath = stroker.createStroke(strokePath);
paintPen.setDashOffset(animFrame);
painter->fillPath(fillPath, QBrush(QColor(255,0,0)));
painter->fillPath(strokePath, QBrush(QColor(0,255,0)));
}
QPointF Arrow::objectEndPoint1()
{
return scenePos();
}
QPointF Arrow::objectEndPoint2()
{
QLineF lyne = line();
qreal rot = radians(rotation());
qreal cosRot = qCos(rot);
qreal sinRot = qSin(rot);
qreal x2 = lyne.x2();
qreal y2 = lyne.y2();
qreal rotEnd2X = x2*cosRot - y2*sinRot;
qreal rotEnd2Y = x2*sinRot + y2*cosRot;
return (scenePos() + QPointF(rotEnd2X, rotEnd2Y));
}
void Arrow::setObjectEndPoint1(qreal x1, qreal y1)
{
QPointF endPt2 = objectEndPoint2();
qreal x2 = endPt2.x();
qreal y2 = endPt2.y();
qreal dx = x2 - x1;
qreal dy = y2 - y1;
setRotation(0);
setLine(0, 0, dx, dy);
setPos(x1, y1);
}
void Arrow::setObjectEndPoint2(qreal x2, qreal y2)
{
QPointF endPt1 = scenePos();
qreal x1 = endPt1.x();
qreal y1 = endPt1.y();
qreal dx = x2 - x1;
qreal dy = y2 - y1;
setRotation(0);
setLine(0, 0, dx, dy);
setPos(x1, y1);
}
Upvotes: 1