Go back to Richel Bilderbeek's homepage.

Go back to Richel Bilderbeek's C++ page.

 

 

 

 

 

(C++) Qt QGraphicsPathItem example 5: Bezier quadratic lines with arrow heads

 

QGraphicsPathItem example 5: Bezier quadratic lines with arrow heads is a QGraphicsPathItem example. This example shows how to use QGraphicsRectItems to manipulatie QGraphicsPathItems, resulting in quadratic Bezier curves.

 

 

 

 

 

 

The math

 

Math figure

 

The arrow can be manipulated by three rectangles, called 'from', 'mid' and 'to'. To let the quadratic Bezier curve go through 'mid', it uses 'beyond' as its hinge point. 'beyond' is the mirror point of 'center', using 'mid' as the mirror. 'center' is the point between 'from' and 'to'.

 

 

 

 

 

Technical facts

 

Application type(s)

Operating system(s) or programming environment(s)

IDE(s):

Project type:

C++ standard:

Compiler(s):

Libraries used:

 

 

 

 

 

Qt project file: CppQGraphicsPathItemExample5.pro

 

QT       += core gui
QMAKE_CXXFLAGS += -std=c++11 -Wall -Wextra -Werror
TARGET = CppQGraphicsPathItemExample5
TEMPLATE = app

SOURCES += \
    qtmain.cpp \
    qtwidget.cpp \
    qtpathitem.cpp \
    qtrectitem.cpp

HEADERS += \
    qtwidget.h \
    qtpathitem.h \
    qtrectitem.h

 

 

 

 

 

qtmain.cpp

 

#ifdef _WIN32
//See http://www.richelbilderbeek.nl/CppCompileErrorSwprintfHasNotBeenDeclared.htm
#undef __STRICT_ANSI__
#endif

#include <QApplication>
#include <QDesktopWidget>
#include "qtwidget.h"

int main(int argc, char *argv[])
{
  QApplication a(argc, argv);
  QtWidget w;
  {
    //Resize the dialog and put it in the screen center
    w.setGeometry(0,0,600,400);
    const QRect screen = QApplication::desktop()->screenGeometry();
    w.move( screen.center() - w.rect().center() );
  }
  w.show();
  return a.exec();
}

 

 

 

 

 

qtpathitem.h

 

#ifndef QTPATHITEM_H
#define QTPATHITEM_H

#include <QGraphicsPathItem>

struct QtRectItem;

struct QtPathItem : public QGraphicsItem
{
  QtPathItem(
    const QtRectItem * const from,
    const bool tail,
    const QtRectItem * const mid,
    const bool head,
    const QtRectItem * const to,
    QGraphicsItem *parent = 0, QGraphicsScene *scene = 0);

  ///Respond to key press
  void keyPressEvent(QKeyEvent *event);

  ///Respond to mouse press
  void mousePressEvent(QGraphicsSceneMouseEvent *event);


  protected:
  ///Change the cursor when the user moves the mouse cursor in the bounding rectangle
  void hoverEnterEvent(QGraphicsSceneHoverEvent *event);

  ///The rectangle that containg the item, used for rough calculations like
  ///collision detection
  QRectF boundingRect() const;

  ///Paint a QtPathItem
  void paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *);

  ///More precise shape compared to boundingRect
  ///In this example, it is redefined to ease selecting those thin lines
  QPainterPath shape() const;

  private:
  ///The extra width given to the line for easier clicking
  static const double m_click_easy_width;

  ///The item where the arrow originates from
  const QtRectItem * const m_from;

  ///Show arrow at head
  bool m_head;

  ///The item where the arrow pass through in the middle
  const QtRectItem * const m_mid;

  ///Show arrow at tail
  bool m_tail;

  ///The item where the arrow points to
  ///(would the arrow and tail heads not be reversible)
  const QtRectItem * const m_to;

  ///Obtain the angle in radians between two deltas
  ///12 o'clock is 0.0 * pi
  /// 3 o'clock is 0.5 * pi
  /// 6 o'clock is 1.0 * pi
  /// 9 o'clock is 1.5 * pi
  //From www.richelbilderbeek.nl/CppGetAngle.htm
  static double GetAngle(const double dx, const double dy);

  ///Obtain point 'beyond'
  QPointF GetBeyond() const;

  ///Obtain point 'center'
  QPointF GetCenter() const;

  ///Obtain the head point of the arrow, on the edge of the rectangle m_from
  QPointF GetHead() const;

  ///Obtain the tail point of the arrow, on the edge of the rectangle m_from
  QPointF GetTail() const;
};

#endif // QTPATHITEM_H

 

 

 

 

 

qtpathitem.cpp

 

#ifdef _WIN32
//See http://www.richelbilderbeek.nl/CppCompileErrorSwprintfHasNotBeenDeclared.htm
#undef __STRICT_ANSI__
#endif

//#include own header file as first substantive line of code, from:
// * John Lakos. Large-Scale C++ Software Design. 1996. ISBN: 0-201-63362-0. Section 3.2, page 110
#include "qtpathitem.h"

#include <cassert>
#include <cmath>

#include <QCursor>
#include <QGraphicsSceneMouseEvent>
#include <QKeyEvent>
#include <QPainter>

#include "qtrectitem.h"

#include <boost/geometry.hpp>
#include <boost/geometry/geometries/linestring.hpp>
#include <boost/geometry/geometries/point_xy.hpp>

///Obtain the zero or one intersections between two finite lines
//From http://www.richelbilderbeek.nl/CppGetLineLineIntersections.htm
template <class T>
const std::vector<
  boost::geometry::model::d2::point_xy<T>
>
GetLineLineIntersections(
  const boost::geometry::model::linestring<
    boost::geometry::model::d2::point_xy<T>
  > line1,
  const boost::geometry::model::linestring<
    boost::geometry::model::d2::point_xy<T>
  > line2)
{
  typedef boost::geometry::model::d2::point_xy<T> Point;
  typedef boost::geometry::model::linestring<Point> Line;
  std::vector<Point> points;
  boost::geometry::intersection(line1,line2,points);
  assert(points.empty() || points.size() == 1);
  return points;
}

//Helper function to create a const line without a temporary std::vector
template <class T>
const boost::geometry::model::linestring<boost::geometry::model::d2::point_xy<T>
>
CreateLine(const std::vector<boost::geometry::model::d2::point_xy<T> >& v)
{
  return boost::geometry::model::linestring<
    boost::geometry::model::d2::point_xy<T>
  >(v.begin(),v.end());
}

///Obtain the zero, one or two intersections between a line and a rectanle
//From http://www.richelbilderbeek.nl/CppGetLineRectIntersections.htm
template <class T>
const std::vector<
  boost::geometry::model::d2::point_xy<T>
>
GetLineRectIntersections(
  const boost::geometry::model::linestring<
    boost::geometry::model::d2::point_xy<T>
  > line,
  const boost::geometry::model::box<
    boost::geometry::model::d2::point_xy<T>
  > rect)
{
  typedef boost::geometry::model::d2::point_xy<T> Point;
  typedef boost::geometry::model::linestring<Point> Line;
  typedef boost::geometry::model::box<Point> Rect;

  const Point p0 = Point(rect.min_corner().x(), rect.min_corner().y());
  const Point p1 = Point(rect.max_corner().x(), rect.min_corner().y());
  const Point p2 = Point(rect.min_corner().x(), rect.max_corner().y());
  const Point p3 = Point(rect.max_corner().x(), rect.max_corner().y());
  const std::vector<Line> lines
    =
    {
      CreateLine(std::vector<Point>( {p0,p1} )),
      CreateLine(std::vector<Point>( {p0,p2} )),
      CreateLine(std::vector<Point>( {p1,p3} )),
      CreateLine(std::vector<Point>( {p2,p3} ))
    };
  std::vector<Point> points;
  std::for_each(lines.begin(),lines.end(),
    [&points,line](const Line& side)
    {
      const std::vector<Point> v = GetLineLineIntersections(line,side);
      std::copy(v.begin(),v.end(),std::back_inserter(points));
    }
  );
  //Remove doublures
  //Put 'typename' before 'std::vector<Point>::iteratortype' to prevent getting the error below:
  //error: need 'typename' before 'std::vector<boost::geometry::model::d2::point_xy<T> >::iterator'
  //  because 'std::vector<boost::geometry::model::d2::point_xy<T> >' is a dependent scope
  typename std::vector<Point>::iterator new_end = std::unique( points.begin(),points.end(),
    [](const Point& lhs, const Point& rhs)
    {
      return lhs.x() == rhs.x() && lhs.y() == rhs.y();
    }
  );
  points.erase(new_end,points.end());

  assert(points.size() <= 2);

  return points;
}


const double QtPathItem::m_click_easy_width = 10.0;

QtPathItem::QtPathItem(
  const QtRectItem * const from,
  const bool tail,
  const QtRectItem * const mid,
  const bool head,
  const QtRectItem * const to,
  QGraphicsItem *parent, QGraphicsScene *scene)
  : QGraphicsItem(parent,scene),
    m_from(from),
    m_head(head),
    m_mid(mid),
    m_tail(tail),
    m_to(to)
{
  this->setFlags(QGraphicsItem::ItemIsSelectable);

  assert(!(flags() & QGraphicsItem::ItemIsMovable) );
  assert( (flags() & QGraphicsItem::ItemIsSelectable) );

  //Accept enterHoverEvents
  this->setAcceptHoverEvents(true);

  //Put this arrow item under the rect
  this->setZValue(mid->zValue() - 1.0);
}

QRectF QtPathItem::boundingRect() const
{
  return shape().boundingRect();
}

double QtPathItem::GetAngle(const double dx, const double dy)
{
  return M_PI - (std::atan(dx/dy));
}

QPointF QtPathItem::GetBeyond() const
{
  const QPointF center = GetCenter();
  const double dx_mid_center = m_mid->pos().x() - center.x();
  const double dy_mid_center = m_mid->pos().y() - center.y();
  const QPointF beyond(center.x() + dx_mid_center + dx_mid_center, center.y() + dy_mid_center + dy_mid_center);
  return beyond;
}

QPointF QtPathItem::GetCenter() const
{
  const QPointF center((m_from->pos() + m_to->pos()) / 2.0);
  return center;
}

QPointF QtPathItem::GetHead() const
{
  typedef boost::geometry::model::d2::point_xy<double> Point;
  typedef boost::geometry::model::linestring<Point> Line;
  typedef boost::geometry::model::box<Point> Rect;

  const QPointF beyond = GetBeyond();

  const Line line_head = CreateLine(
    std::vector<Point>(
      {
        Point(beyond.x(),beyond.y()),
        Point(m_to->pos().x(),m_to->pos().y()),
      }
    )
  );

  const QRectF qr_to = m_to->boundingRect().translated(m_to->pos());

  const Rect r_to(
    Point(qr_to.topLeft().x()    ,qr_to.topLeft().y()    ),
    Point(qr_to.bottomRight().x(),qr_to.bottomRight().y())
    );


  std::vector<Point> p_head_end = GetLineRectIntersections(line_head,r_to);
  if (p_head_end.empty())
  {
    p_head_end.push_back(Point(m_to->pos().x(),m_to->pos().y()));
    //Yes,it happens, when the line does not leave the rectangle
    //this happens when the two node rectanges overlap
  }
  assert(!p_head_end.empty());
  assert(p_head_end.size() == 1);
  return QPointF(p_head_end[0].x(),p_head_end[0].y());
}

QPointF QtPathItem::GetTail() const
{
  typedef boost::geometry::model::d2::point_xy<double> Point;
  typedef boost::geometry::model::linestring<Point> Line;
  typedef boost::geometry::model::box<Point> Rect;

  const QPointF beyond = GetBeyond();

  const Line line_tail = CreateLine(
    std::vector<Point>(
      {
        Point(m_from->pos().x(),m_from->pos().y()),
        Point(beyond.x(),beyond.y()),
      }
    )
  );

  const QRectF qr_from = m_from->boundingRect().translated(m_from->pos());

  const Rect r_from(
    Point(qr_from.topLeft().x()    ,qr_from.topLeft().y()    ),
    Point(qr_from.bottomRight().x(),qr_from.bottomRight().y())
    );

  std::vector<Point> p_tail_end = GetLineRectIntersections(line_tail,r_from);
  if (p_tail_end.empty())
  {
    //Yes,it happens, when the line does not leave the rectangle
    //this happens when the two node rectanges overlap
    p_tail_end.push_back(Point(m_from->pos().x(),m_from->pos().y()));
  }
  assert(!p_tail_end.empty());
  assert(p_tail_end.size() == 1);
  return QPointF(p_tail_end[0].x(),p_tail_end[0].y());
}

void QtPathItem::hoverEnterEvent(QGraphicsSceneHoverEvent *)
{
  this->setCursor(QCursor(Qt::PointingHandCursor));
}

void QtPathItem::keyPressEvent(QKeyEvent *event)
{
  switch (event->key())
  {
    case Qt::Key_F1:
    case Qt::Key_1:
    case Qt::Key_T:
    case Qt::Key_Minus:
      m_tail = !m_tail;
      this->update();
      break;
    case Qt::Key_F2:
    case Qt::Key_2:
    case Qt::Key_H:
    case Qt::Key_Plus:
      m_head = !m_head;
      this->update();
      break;
    default:
      break;
  }
  QGraphicsItem::keyPressEvent(event);
}

void QtPathItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
  if (event->modifiers() & Qt::ShiftModifier)
  {
    if ((event->pos() - this->m_from->pos()).manhattanLength() < 10.0)
    {
      m_tail = !m_tail;
      this->update();
    }
    else if ((event->pos() - this->m_to->pos()).manhattanLength() < 10.0)
    {
      m_head = !m_head;
      this->update();
    }
  }
  QGraphicsItem::mousePressEvent(event);
}


void QtPathItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *)
{
  painter->setRenderHint(QPainter::Antialiasing);

  if (this->isSelected())
  {
    const QColor color(255,0,0);
    QPen pen;
    pen.setColor(color);
    pen.setWidth(3);
    painter->setPen(pen);
  }
  else
  {
    const QColor color(0,0,0);
    QPen pen;
    pen.setColor(color);
    pen.setWidth(1);
    painter->setPen(pen);
  }



  //Line must go _though_ mid pos, instead of using it as a virtual hinge point
  //Solution:
  // - define point 'center' as the middle between from and to
  // - define point 'beyond' as the mirror point of 'center', using mid_pos as a mirror
  const QPointF beyond = GetBeyond();
  const QPointF p_tail_end = GetTail();
  const QPointF p_head_end = GetHead();

  QPainterPath curve;
  curve.moveTo(p_tail_end);
  curve.quadTo(beyond,p_head_end);
  painter->drawPath(curve);

  {
    const double sz = 10.0; //pixels
    if (m_tail)
    {
      //The angle from midpoint to tail
      //Thanks goes out to Toine van den Bogaart and Theo van den Bogaart for being happy to help with the math
      const double dx = beyond.x() - m_from->pos().x();
      const double dy = beyond.y() - m_from->pos().y();
      double angle = GetAngle(dx,dy);
      if (dy >= 0.0) angle = (1.0 * M_PI) + angle;
      const QPointF p0(p_tail_end.x(),p_tail_end.y());
      const QPointF p1
        = p0 + QPointF(
           std::sin(angle + M_PI + (M_PI * 0.1)) * sz,
          -std::cos(angle + M_PI + (M_PI * 0.1)) * sz);
      const QPointF p2
        = p0 + QPointF(
           std::sin(angle + M_PI - (M_PI * 0.1)) * sz,
          -std::cos(angle + M_PI - (M_PI * 0.1)) * sz);
      painter->drawPolygon(QPolygonF() << p0 << p1 << p2);
    }
    if (m_head)
    {
      //The angle from midpoint to head
      //Thanks goes out to Toine van den Bogaart and Theo van den Bogaart for being happy to help with the math
      const double dx = m_to->pos().x() - beyond.x();
      const double dy = m_to->pos().y() - beyond.y();
      double angle = GetAngle(dx,dy);
      if (dy >= 0.0) angle = (1.0 * M_PI) + angle;

      const QPointF p0(p_head_end.x(),p_head_end.y());
      const QPointF p1
        = p0 + QPointF(
           std::sin(angle +  0.0 + (M_PI * 0.1)) * sz,
          -std::cos(angle +  0.0 + (M_PI * 0.1)) * sz);
      const QPointF p2
        = p0 + QPointF(
           std::sin(angle +  0.0 - (M_PI * 0.1)) * sz,
          -std::cos(angle +  0.0 - (M_PI * 0.1)) * sz);

      painter->drawPolygon(QPolygonF() << p0 << p1 << p2);
    }
  }
}

QPainterPath QtPathItem::shape() const
{
  const QPointF beyond = GetBeyond();
  const QPointF p_tail_end = GetTail();
  const QPointF p_head_end = GetHead();

  QPainterPath curve;
  curve.moveTo(p_tail_end);
  curve.quadTo(beyond,p_head_end);

  QPainterPathStroker stroker;
  stroker.setWidth(m_click_easy_width);
  return stroker.createStroke(curve);
}

 

 

 

 

 

qtrectitem.h

 

#ifndef QTRECTITEM_H
#define QTRECTITEM_H

#include <QGraphicsRectItem>
#include <boost/signals2.hpp>

struct QtRectItem : public QGraphicsRectItem
{
  QtRectItem(QGraphicsItem *parent = 0, QGraphicsScene *scene = 0);

  boost::signals2::signal<void()> m_signal_mouse_move;
  protected:
  void mouseMoveEvent(QGraphicsSceneMouseEvent *event);
};

#endif // QTRECTITEM_H

 

 

 

 

 

qtrectitem.cpp

 

#ifdef _WIN32
//See http://www.richelbilderbeek.nl/CppCompileErrorSwprintfHasNotBeenDeclared.htm
#undef __STRICT_ANSI__
#endif

//#include own header file as first substantive line of code, from:
// * John Lakos. Large-Scale C++ Software Design. 1996. ISBN: 0-201-63362-0. Section 3.2, page 110
#include "qtrectitem.h"

QtRectItem::QtRectItem(QGraphicsItem *parent, QGraphicsScene *scene)
: QGraphicsRectItem(parent,scene)
{
  this->setFlags(
      QGraphicsItem::ItemIsSelectable
    | QGraphicsItem::ItemIsMovable);

  const double length = 8;
  this->setRect(-length/2.0,-length/2.0,length,length);
}

void QtRectItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
  m_signal_mouse_move();
  QGraphicsRectItem::mouseMoveEvent(event);
}

 

 

 

 

 

qtwidget.h

 

#ifndef QTWIDGET_H
#define QTWIDGET_H

#include <QGraphicsView>

///The widget holding the items
struct QtWidget : public QGraphicsView
{
  QtWidget(QWidget *parent = 0);

  ///Pass the key presses to the scene items
  void keyPressEvent(QKeyEvent *event);

  private:
  void OnMouseMove();
};

#endif // QTWIDGET_H

 

 

 

 

 

qtwidget.cpp

 

#ifdef _WIN32
//See http://www.richelbilderbeek.nl/CppCompileErrorSwprintfHasNotBeenDeclared.htm
#undef __STRICT_ANSI__
#endif

//#include own header file as first substantive line of code, from:
// * John Lakos. Large-Scale C++ Software Design. 1996. ISBN: 0-201-63362-0. Section 3.2, page 110
#include "qtwidget.h"


#include <cassert>
#include <cmath>
#include <QGraphicsScene>
#include "qtrectitem.h"
#include "qtpathitem.h"

QtWidget::QtWidget(QWidget *parent)
  : QGraphicsView(new QGraphicsScene,parent)
{
  const int n_items = 18;
  std::vector<QtRectItem *> rects;

  for (int i=0; i!=n_items; ++i)
  {
    const double angle = 2.0 * M_PI * (static_cast<double>(i) / static_cast<double>(n_items));
    const double x1 =  std::sin(angle) * 100.0;
    const double y1 = -std::cos(angle) * 100.0;
    QtRectItem * const rect = new QtRectItem;
    rect->setPos(x1,y1);
    scene()->addItem(rect);
    rects.push_back(rect);
    rect->m_signal_mouse_move.connect(
      boost::bind(&QtWidget::OnMouseMove,this));
  }
  for (int i=0; i<n_items-2; i+=3)
  {
    assert(i + 2 < n_items);
    QtPathItem * const item = new QtPathItem(
      rects[(i+0) % n_items],
      false,
      rects[(i+1) % n_items],
      true,
      rects[(i+2) % n_items]);
    scene()->addItem(item);
  }
}

void QtWidget::keyPressEvent(QKeyEvent *event)
{
  QList<QGraphicsItem *> v = scene()->selectedItems();
  std::for_each(v.begin(),v.end(),
    [event](QGraphicsItem * const item)
    {
      if (QtPathItem * const pathitem = dynamic_cast<QtPathItem *>(item))
      {
        pathitem->keyPressEvent(event);
      }
    }
  );
}

void QtWidget::OnMouseMove()
{
  this->scene()->update();
}

 

 

 

 

 

crosscompiletowindows.sh

 

#!/bin/sh
#From http://richelbilderbeek.nl/CppQtCrosscompileToWindowsExample15.htm

echo "Cross compiling to Windows: developer version"

echo "1/2: Creating Windows makefile"
i686-pc-mingw32-qmake CppQGraphicsPathItemExample5.pro

echo "2/2: making makefile"

make

echo "Done"

 

 

 

 

 

Go back to Richel Bilderbeek's C++ page.

Go back to Richel Bilderbeek's homepage.

 

Valid XHTML 1.0 Strict

This page has been created by the tool CodeToHtml