0

I need to draw a connector between two items in a QSceneView. I've subclssed QGraphicsObject for creating this line (it's not a QGraphicsLineItem because I need to update its appearance later). This is the class:

#ifndef TRANSITIONDRAGLINE_HPP_
#define TRANSITIONDRAGLINE_HPP_

#include "Style/CanvasTransitionDragLine.hpp"
#include <QGraphicsObject>
#include <QPointF>
#include <string>

class TransitionDragLine : public QGraphicsObject {

  Q_OBJECT

public:

  TransitionDragLine(const Style::CanvasTransitionDragLine& style, QGraphicsItem* parent = nullptr);
  virtual ~TransitionDragLine();
  void setStartPoint(const QPointF& startPoint);
  void setEndPoint(const QPointF& endPoint);
public:

  QRectF boundingRect() const override;
  void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) override;

private:

  Style::CanvasTransitionDragLine m_style;
  QPointF m_startPoint;
  QPointF m_endPoint;
};

#endif // !TRANSITIONDRAGLINE_HPP_
#include "TransitionDragLine.hpp"
#include <QPainter>

///////////////////////////////////////////////////////////////////////////////
// PUBLIC SECTION                                                            //
///////////////////////////////////////////////////////////////////////////////

TransitionDragLine::TransitionDragLine(const Style::CanvasTransitionDragLine& style, QGraphicsItem* parent) :
  QGraphicsObject(parent),
  m_style(style) {

}

TransitionDragLine::~TransitionDragLine() {

}

void TransitionDragLine::setStartPoint(const QPointF& startPoint) {
  m_startPoint = startPoint;
}

void TransitionDragLine::setEndPoint(const QPointF& endPoint) {
  m_endPoint = endPoint;
  update();
}

///////////////////////////////////////////////////////////////////////////////
// VIRTUAL PUBLIC SECTION                                                    //
///////////////////////////////////////////////////////////////////////////////

QRectF TransitionDragLine::boundingRect() const {
  qreal dx = m_endPoint.x() - m_startPoint.x();
  qreal dy = m_endPoint.y() - m_startPoint.y();
  qreal x{ 0.0 };
  qreal y{ 0.0 };
  qreal w{ 0.0 };
  qreal h{ 0.0 };
  qreal penHalfWidth{ m_style.getPen().widthF() * 0.5 };

  if (dx >= 0.0 && dy >= 0.0) {
    x = 0.0 - penHalfWidth;
    y = 0.0 - penHalfWidth;
    w = dx + penHalfWidth;
    h = dy + penHalfWidth;
  }
  else if (dx >= 0.0 && dy < 0.0) {
    x = 0.0 - penHalfWidth;
    y = dy - penHalfWidth;
    w = dx - penHalfWidth;
    h = -dy - penHalfWidth;
  }
  else if (dx < 0.0 && dy >= 0.0) {
    x = dx - penHalfWidth;
    y = 0 - penHalfWidth;
    w = -dx - penHalfWidth;
    h = dy - penHalfWidth;
  }
  else {
    x = dx - penHalfWidth;
    y = dy - penHalfWidth;
    w = -dx - penHalfWidth;
    h = -dy - penHalfWidth;
  }
  return QRectF(x, y, w, h);
}
void TransitionDragLine::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) {
  qreal px = m_endPoint.x() - m_startPoint.x();
  qreal py = m_endPoint.y() - m_startPoint.y();
  painter->setPen(m_style.getPen());
  painter->drawLine(0.0, 0.0, px, py);
}

When I want to use it, I set a flag in my Canvas class, that inherits QGraphicsView, and I update it. When I press the mouse button I set the start point and then I set the endpoint (and call update()), every time that I move the mouse until left button is pressed. Basically I call startTransitionLineDrag in polling until the mouse is pressed, and when I release the mouse button I call endTransitionLineDrag in my Canvas (subclass of QGraphicsView):

void Canvas::mouseMoveEvent(QMouseEvent* event) {
  switch (m_currentState) {
  case CurrentState::PressedOnTransition: {
    if (event->buttons() == Qt::LeftButton) {
      startTransitionLineDrag(event);
    }
  } break;
  }
  QGraphicsView::mouseMoveEvent(event);
}

void Canvas::mouseReleaseEvent(QMouseEvent* event) {
  if (m_currentState == CurrentState::PressedOnTransition) {
    if (true /* business logic here not useful for the problem */) {
      endTransitionLineDrag(event);
    }
  }
  QGraphicsView::mouseReleaseEvent(event);
}

void Canvas::startTransitionLineDrag(QMouseEvent* event) {
  if (m_transitionDragLine == nullptr) {
    m_transitionDragLine = new TransitionDragLine(m_style.getTransitionDragLineStyle());
    m_transitionDragLine->setStartPoint(mapToScene(event->pos()));
    m_scene->addItem(m_transitionDragLine);
    m_transitionDragLine->setPos(mapToScene(event->pos()));
  }
  m_transitionDragLine->setEndPoint(mapToScene(event->pos()));
  //repaint();
}

void Canvas::endTransitionLineDrag(QMouseEvent* event) {
  /* other business logic */
  deleteDragTransitionLine();
}

void Canvas::deleteDragTransitionLine() {
  if (m_transitionDragLine) {
    m_scene->removeItem(m_transitionDragLine);
    delete m_transitionDragLine;
    m_transitionDragLine = nullptr;
  }
}

The logic works: when I activate the dragging I can see the line and it's update until the mouse button is pressed. But you can see in the attached image that I've a rendering problem:

dragline defect

The rendering is not working properly for the line; I've a trail of past images of the line, like the QGraphicsView is not updated properly.

I sense a code smell when in TransitionDragLine::setEndPoint I need to call update() after setting the end point of the line (otherwise the line is not displayed) but I don't find a way to solve the issue.

What I'm doing wrong?

EDIT:

I've seen a solution here:

Artifacts showing when modifying a custom QGraphicsItem

I've tried to call prepareGeometryChange() every time that I update the end point but artifacts remain:

void TransitionDragLine::setEndPoint(const QPointF& endPoint) {
  m_endPoint = endPoint;
  prepareGeometryChange();
  update();
}
Jepessen
  • 11,744
  • 14
  • 82
  • 149
  • Does setting [`QGraphicsView::updateMode`](https://doc.qt.io/qt-5/qgraphicsview.html#viewportUpdateMode-prop) to [`QGraphicsView::FullViewportUpdate`](https://doc.qt.io/qt-5/qgraphicsview.html#ViewportUpdateMode-enum) help? Most likely, your bounding boxes are incorrect. With `FullViewportUpdate`, the repainting does not rely on the bounding boxes. – pasbi May 12 '20 at 10:07
  • I love you!!! Can you make an answer for accepting it? – Jepessen May 12 '20 at 10:11

1 Answers1

1

It looks like your implementation of QRectF TransitionDragLine::boundingRect() const is incorrect. It can be a bit tricky to get it right since multiple coordinate systems are involved.

The proper solution would be to get the bounding boxes right, but for small scenes, it's usually good enough to set the QGraphicsView::updateMode to QGraphicsView::FullViewportUpdate. That is, QGraphicsView will redraw the whole viewport rather than just the bounding box area.

pasbi
  • 2,037
  • 1
  • 20
  • 32
  • Why my bounding rect is wrong? If I draw it with `drawRect` in `paint` I obtain the right rectangle. The purpose is to give to bounding rect always the upper left corner and positive width and height regarding position of start point and end point of my line. – Jepessen May 12 '20 at 10:22
  • I don't understand why you need the if-cascade to compute the bounding box, but not to paint the line. I'd define the bounding box similarly to the line in your paint method (`QRectF(0, 0, px, py)`) and maybe add the width of the pen. It can also be helpful for debugging to draw the bounding box (in `TransitionDragLine::paint`: `painter->drawRect(boundingRect());`) Ah and regarding positive width/height: [`QRectF::normalized`](https://doc.qt.io/qt-5/qrectf.html#normalized). – pasbi May 12 '20 at 10:28