paulm
paulm

Reputation: 5882

Finding the outline of a QGraphicsItem

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:

enter image description here

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:

  1. Store the line angle.
  2. Set the angle to 0.
  3. Copy the line.
  4. Use QLineF::translate() on the copy.
  5. Set both lines angles back to the value you stored in 1 - this then causes the start and end pos of each line to be misaligned.

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.

enter image description here

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

Answers (2)

paulm
paulm

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

redteam316
redteam316

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

Related Questions