Search code examples
c++qt4geometryqgraphicsitem

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!


Solution

  • 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