overriding paint() and mouseEvents() of a QGraphicsItem - c++

In Qt, I'm trying to highlight an item when I'm clicking it and then draw a line from its end to the current cursor position. I can either highlight it or draw the line, but not both. Here are my classes :
// ucPin.h
#pragma once
#include "stdafx.h"
class ucPin : public QGraphicsLineItem{
qreal x1, y1, x2, y2;
bool isClicked;
QGraphicsLineItem * li;
public :
ucPin (qreal x1, qreal y1, qreal x2, qreal y2, QGraphicsItem *parent = 0);
void paint ( QPainter * painter, const QStyleOptionGraphicsItem * option, QWidget * widget );
void mousePressEvent (QGraphicsSceneMouseEvent * event);
void mouseReleaseEvent (QGraphicsSceneMouseEvent *event);
void mouseMoveEvent (QGraphicsSceneMouseEvent *event);
};
// ucPin.cpp
#include "ucPin.h"
ucPin::ucPin (qreal x1, qreal y1, qreal x2, qreal y2, QGraphicsItem *parent) :
QGraphicsLineItem(x1, y1, x2, y2, parent){
this->x1 = x1; this->y1 = y1; this->x2 = x2; this->y2 = y2;
this->setFlags(QGraphicsItem::ItemIsFocusable
| QGraphicsItem::ItemIsSelectable);
isClicked = false;
li = NULL;
}
void ucPin::paint ( QPainter * painter, const QStyleOptionGraphicsItem * option, QWidget * widget )
{
QStyleOptionGraphicsItem newOption (*option);
newOption.state =QStyle::State_None;
QGraphicsLineItem::paint(painter, &newOption, widget);
//painter-> setRenderHint(QPainter::Antialiasing);
if (option->state & QStyle::State_Selected) {
QPen outline;
outline.setColor(Qt::green);
outline.setWidth(6);
setPen(outline);
}
else{
QPen outline; outline.setWidth(3);
setPen(outline);
}
}
void ucPin::mousePressEvent(QGraphicsSceneMouseEvent* event) {
/*QPointF pos = event->pos();
QGraphicsLineItem * li = new QGraphicsLineItem(0,0, pos.x(), pos.y(), this);*/
isClicked = true;
}
void ucPin::mouseReleaseEvent(QGraphicsSceneMouseEvent* event) {
isClicked = false;
}
void ucPin::mouseMoveEvent(QGraphicsSceneMouseEvent* event) {
if(NULL != li) { delete li; li = NULL;}
li = new QGraphicsLineItem(0,0, event->pos().x(), event->pos().y(), this);
}
I guess when implementing the mouseEvent overrides, the paint event for the selected state doesn't get called? How can one work around this?

In order to request the repainting of your QGraphicsItem, you need to call update() whenever the appearance changes. The setSelected is called in QGraphicsItem::mousePressEvent, so you need to propagate it to the base class calling QGraphicsLineItem::mousePressEvent(event) in your overriden mousePressEvent. Same goes for mouseReleaseEvent. Or you could handle the selection state yourself, but you shouldn't unless there's a really good reason for that. Also, I think it might make sense to set your pens before calling QGraphicsLineItem::paint(...).
Also, it is inefficient to allocate a new li every time the mouse moves - just allocate it once and use setLine to set the new coordinates.

Related

Error : Non-static member found in multiple base-class subobjects of type QGraphicsItem

I am trying to achieve rubber band rectangle zoom in. But here I need to zoom in only the part which comes under rubber band rectangle. I do not want to zoom in the whole view.
For that I was told to create rubber band rectangle whose point I will know. And then use
void QGraphicsView::ensureVisible(qreal x, qreal y, qreal w, qreal h, int xmargin = 50, int ymargin = 50)
or
void QGraphicsView::centerOn(qreal x, qreal y)
So just to understand, what this ensureVisible() does, I called it with some points which are present in my design. But just a pop up windows appeared but nothing was there to display. So I left that task and concentrated on, how to get rubber band rectagle points. So I tried to add some mouse events in my existing sample project but I got following error.
schematicdesign.cpp:75:47: error: non-static member 'mapToScene' found
in multiple base-class subobjects of type 'QGraphicsItem': class
SchematicDesign -> class QGraphicsRectItem -> class
QAbstractGraphicsShapeItem -> class QGraphicsItem class
SchematicDesign -> class QGraphicsPathItem -> class
QAbstractGraphicsShapeItem -> class QGraphicsItem
qgraphicsitem.h:360:13: note: member found by ambiguous name lookup
Such error is not only for mapToScene() but also for scene() and transform().
What I understood from the error is :
I have inherited schematicDesign class from multiple classes (
QGraphicsRectItem and QGraphicsPathItem ) so when it tries to find the
definition of mapToScene() or scene() or transform(), it finds in
multiple classes so gets confused in which one to pick. ( please
correct if I am wrong )
Here is my code:
Widget.h
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QGraphicsView
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private slots:
void on_schematicButoon_clicked();
private:
Ui::Widget *ui;
QGraphicsScene* scene;
QGraphicsView* view;
};
Widget.cpp
Widget::Widget(QWidget *parent)
: QGraphicsView(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
scene = new QGraphicsScene(this);
view = new QGraphicsView(this);
view->setScene(scene);
ui->verticalLayout_2->addWidget(view);
}
void Widget::on_schematicButoon_clicked()
{
SchematicDesign* sd1 = new SchematicDesign();
QGraphicsRectItem* clkPin = sd1->createRect(20,160,20,20);
scene->addItem(clkPin);
// some more createRect() method
QPen mPen;
mPen.setWidth(3);
mPen.setBrush(Qt::yellow);
QPolygonF clknet;
clknet<< QPointF (40,170) << QPointF (186,170);
QGraphicsPathItem* clk = sd1->drawPolyline(clknet);
scene->addItem(clk);
// some more drawPolyline() method.
QGraphicsPathItem* text = sd1->addText(i1Instance,275.0,138.0,"I1");
scene->addItem(text);
// some more addText() method
}
SchematicDesign.h
class SchematicDesign : public QGraphicsRectItem,QGraphicsPathItem
{
public:
explicit SchematicDesign();
explicit SchematicDesign(qreal x, qreal y, qreal width, qreal height,QGraphicsItem *parent = nullptr)
: QGraphicsRectItem(x, y, width, height, parent)
{}
explicit SchematicDesign(QPainterPath pPath);
signals:
public:
virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget);
QGraphicsRectItem* createRect(qreal x, qreal y, qreal width, qreal height);
QGraphicsPathItem* drawPolyline(QPolygonF poly);
QGraphicsPathItem* addText(QGraphicsRectItem *rect, double d1, double d2, QString s);
protected:
void mousePressEvent(QGraphicsSceneMouseEvent *event);
void mouseMoveEvent(QGraphicsSceneMouseEvent *event);
void mouseReleaseEvent(QGraphicsSceneMouseEvent *event);
private:
QPointF selectTopLeft;
bool drawingSelection;
QGraphicsRectItem* lastRect;
public:
QPoint rubberBandOrigin;
bool rubberBandActive;
};
#endif // SCHEMATICDESIGN_H
SchematicDesign.cpp
SchematicDesign::SchematicDesign(){}
SchematicDesign::SchematicDesign(QPainterPath pPath)
{}
void SchematicDesign::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
auto copied_option = *option;
copied_option.state &= ~QStyle::State_Selected;
auto selected = option->state & QStyle::State_Selected;
QGraphicsRectItem::paint(painter, &copied_option, widget);
QGraphicsPathItem::paint(painter, &copied_option, widget);
if (selected)
{
painter->save();
painter->setBrush(Qt::NoBrush);
painter->setPen(QPen(option->palette.windowText(), 0, Qt::SolidLine));
painter->drawPath(QGraphicsRectItem::shape());
painter->drawPath(QGraphicsPathItem::shape());
painter->restore();
}
}
QGraphicsRectItem *SchematicDesign::createRect(qreal x, qreal y, qreal width, qreal height)
{...... }
QGraphicsPathItem* SchematicDesign::drawPolyline(QPolygonF poly)
{....}
QGraphicsPathItem* SchematicDesign::addText(QGraphicsRectItem *rect, double d1, double d2, QString s)
{.....}
void SchematicDesign::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
QGraphicsItem * sceneItem = scene()->itemAt(mapToScene(event->pos()),transform()); //error
if(!sceneItem){
selectTopLeft = event->pos();
event->pos();
drawingSelection = true;
}
QGraphicsRectItem::mousePressEvent(event);
}
void SchematicDesign::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
{
if(drawingSelection){
QGraphicsItem * itemToRemove = lastRect;
if(itemToRemove){
fitInView(itemToRemove,Qt::KeepAspectRatio); //error at fitInView : Use of undeclared identifier
scene()->removeItem(itemToRemove); //error at scene()
delete itemToRemove;
lastRect = nullptr;
}
}
drawingSelection = false;
QGraphicsRectItem::mouseReleaseEvent(event);
}
void SchematicDesign::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
if(drawingSelection){
//Selection region
int x = selectTopLeft.rx();
int y = selectTopLeft.ry();
int h = event->pos().rx();
int w = event->pos().ry();
//QRect selectRegion = QRect(selectTopLeft,event->pos());
QRect selectRegion = QRect(x,y,h,w);
QPainterPath path;
path.addRect(selectRegion);
scene()->setSelectionArea(mapToScene(path)); //error at scene()
//Draw visual feedback for the user
QGraphicsItem * itemToRemove = lastRect;
scene()->removeItem(itemToRemove); // error at scene()
lastRect = scene()->addRect(QRectF(mapToScene(selectTopLeft),
mapToScene(event->pos())).normalized()); // error at scene() and mapToScene()
lastRect->setBrush(QBrush(QColor(255, 0, 0, 50)));
delete itemToRemove;
}
QGraphicsRectItem::mouseMoveEvent(event);
}
How to tackle such type of error ? Should not I inherit my class from multiple classes ?

Composed QGraphicsItem not detecting clicks on sub-items

I am trying to make a simple extension of QGraphicsRectItem that allows me to resize the rectangle and move it around with the mouse. I model the handles with elliptical arches on the corners I want to enable for dragging, which I implement as QGraphicsEllipseItems:
class QGraphicsBoxWithHandlesItem : public QObject, public QGraphicsRectItem
{
Q_OBJECT
typedef enum {
None,
BottomLeft,
TopRight
} ActiveAnchor;
private:
QGraphicsEllipseItem m_anchorBottomLeft;
QGraphicsEllipseItem m_anchorTopRight;
float m_anchorRadius;
ActiveAnchor m_activeAnchor;
public:
QGraphicsBoxWithHandlesItem(QRectF r, float handlesRadius = 20.0, QGraphicsItem *parent = nullptr);
void setAnchorRadius(float radius);
float getAnchorRadius();
QPainterPath shape() const;
protected:
void mousePressEvent(QGraphicsSceneMouseEvent * event);
void mouseMoveEvent(QGraphicsSceneMouseEvent * event);
void mouseReleaseEvent(QGraphicsSceneMouseEvent * event);
};
I want to be able to detect clicks both on the rectangle and on the handle items (this is necessary because if the rectangle gets too small the handles are the only easily clickable area), so I thought I'd extend QGraphicsRectItem::shape() to add to the returned QPainterPath the paths of the sub-items (adjusting coordinates to make them relative to the parent item):
QPainterPath QGraphicsBoxWithHandlesItem::shape() const
{
auto curShape = QGraphicsRectItem::shape();
curShape.addPath( mapFromItem(&m_anchorBottomLeft, m_anchorBottomLeft.shape()) );
curShape.addPath( mapFromItem(&m_anchorTopRight, m_anchorTopRight.shape()) );
return curShape;
}
What I get, however, is that now clicks inside the handles areas are completely ignored and only clicks in the central area of the rectangle are handled.
What is the correct way to extend the clickable area of an item when it has a non-trivial shape?
Update: I tried to set the ItemIsSelectable flag on a handle and now, if I click on it, I see that it gets selected. I still don't get any mousePressEvent in the parent, however. What am I doing wrong?
Edit:
This is the constructor implementation:
QGraphicsBoxWithHandlesItem::QGraphicsBoxWithHandlesItem( QRectF r, float handlesRadius, QGraphicsItem * parent) :
QGraphicsRectItem(parent),
m_anchorRadius(handlesRadius),
m_activeAnchor(None)
{
setFlag(QGraphicsItem::ItemIsMovable);
setFlag(QGraphicsItem::ItemIsSelectable);
setRect(r);
m_anchorBottomLeft.setRect(-m_anchorRadius, -m_anchorRadius, m_anchorRadius*2, m_anchorRadius*2);
m_anchorBottomLeft.setPos(rect().bottomLeft());
m_anchorBottomLeft.setSpanAngle(90 * 16); // angle is in 16ths of degree
m_anchorBottomLeft.setParentItem(this);
m_anchorTopRight.setRect(-m_anchorRadius, -m_anchorRadius, m_anchorRadius*2, m_anchorRadius*2);
m_anchorTopRight.setPos(rect().topRight());
m_anchorTopRight.setStartAngle(180 * 16); // angle is in 16ths of degree
m_anchorTopRight.setSpanAngle(90 * 16); // angle is in 16ths of degree
m_anchorTopRight.setParentItem(this);
}
Your QGraphicsEllipseItem are on top of the base item so the mouse events will never come to you.
What you have to do is use a sceneEventFilter, but since the QGraphicsEllipseItem are children of the main item they will never move, so they should not have a parent but you should add them directly to the scene.
The full functionality is implemented in the following code:
*.h
#ifndef QGRAPHICSBOXWITHHANDLESITEM_H
#define QGRAPHICSBOXWITHHANDLESITEM_H
#include <QGraphicsRectItem>
#include <QObject>
class QGraphicsBoxWithHandlesItem : public QObject, public QGraphicsRectItem
{
Q_OBJECT
enum ActiveAnchor{
None,
BottomLeft,
TopRight
};
public:
QGraphicsBoxWithHandlesItem(QRectF r, float handlesRadius = 20.0, QGraphicsItem *parent = nullptr);
protected:
QVariant itemChange(GraphicsItemChange change, const QVariant &value);
bool sceneEventFilter(QGraphicsItem *watched, QEvent *event);
private:
QGraphicsEllipseItem m_anchorBottomLeft;
QGraphicsEllipseItem m_anchorTopRight;
float m_anchorRadius;
ActiveAnchor m_activeAnchor;
};
#endif // QGRAPHICSBOXWITHHANDLESITEM_H
*.cpp
#include "qgraphicsboxwithhandlesitem.h"
#include <QEvent>
#include <QGraphicsScene>
#include <QDebug>
QGraphicsBoxWithHandlesItem::QGraphicsBoxWithHandlesItem( QRectF r, float handlesRadius, QGraphicsItem * parent) :
QGraphicsRectItem(parent),
m_anchorRadius(handlesRadius),
m_activeAnchor(None)
{
setFlag(QGraphicsItem::ItemIsMovable);
setFlag(QGraphicsItem::ItemIsSelectable);
setFlag(QGraphicsItem::ItemSendsGeometryChanges);
setRect(r);
m_anchorBottomLeft.setRect(-m_anchorRadius, -m_anchorRadius, m_anchorRadius*2, m_anchorRadius*2);
m_anchorBottomLeft.setPos(rect().bottomLeft());
m_anchorBottomLeft.setSpanAngle(90 * 16); // angle is in 16ths of degree
//m_anchorBottomLeft.setParentItem(this);
m_anchorBottomLeft.setFlag(QGraphicsItem::ItemIsMovable);
m_anchorBottomLeft.setFlag(QGraphicsItem::ItemIsSelectable);
m_anchorTopRight.setRect(-m_anchorRadius, -m_anchorRadius, m_anchorRadius*2, m_anchorRadius*2);
m_anchorTopRight.setPos(rect().topRight());
m_anchorTopRight.setStartAngle(180 * 16); // angle is in 16ths of degree
m_anchorTopRight.setSpanAngle(90 * 16); // angle is in 16ths of degree
//m_anchorTopRight.setParentItem(this);
m_anchorTopRight.setFlag(QGraphicsItem::ItemIsMovable);
m_anchorTopRight.setFlag(QGraphicsItem::ItemIsSelectable);
}
QVariant QGraphicsBoxWithHandlesItem::itemChange(GraphicsItemChange change, const QVariant & value){
if(change == QGraphicsItem::ItemSceneHasChanged){
if(scene()){
scene()->addItem(&m_anchorBottomLeft);
scene()->addItem(&m_anchorTopRight);
m_anchorBottomLeft.installSceneEventFilter(this);
m_anchorTopRight.installSceneEventFilter(this);
}
}
else if (change == QGraphicsItem::ItemPositionHasChanged) {
m_anchorBottomLeft.setPos(mapToScene(rect().bottomLeft()));
m_anchorTopRight.setPos(mapToScene(rect().topRight()));
}
return QGraphicsRectItem::itemChange(change, value);
}
bool QGraphicsBoxWithHandlesItem::sceneEventFilter(QGraphicsItem *watched, QEvent *event)
{
if(watched == &m_anchorTopRight){
switch (event->type()) {
case QEvent::GraphicsSceneMousePress:{
//mousePressEvent
qDebug()<<"mousePressEvent m_anchorTopRight";
break;
}
case QEvent::GraphicsSceneMouseMove:{
// mouseMoveEvent
QRectF r = rect();
auto p = m_anchorTopRight.mapToScene(m_anchorTopRight.rect().center());
r.setTopRight(mapFromScene(p));
setRect(r);
qDebug()<<"mouseMoveEvent m_anchorTopRight";
break;
}
case QEvent::GraphicsSceneMouseRelease :{
//mouseReleaseEvent
qDebug()<<"mouseReleaseEvent m_anchorTopRight";
break;
}
}
}
if(watched == &m_anchorBottomLeft){
switch (event->type()) {
case QEvent::GraphicsSceneMousePress:{
//mousePressEvent
qDebug()<<"mousePressEvent m_anchorBottomLeft";
break;
}
case QEvent::GraphicsSceneMouseMove:{
// mouseMoveEvent
QRectF r = rect();
auto p = m_anchorBottomLeft.mapToScene(m_anchorBottomLeft.rect().center());
r.setBottomLeft(mapFromScene(p));
setRect(r);
qDebug()<<"mouseMoveEvent m_anchorBottomLeft";
break;
}
case QEvent::GraphicsSceneMouseRelease :{
//mouseReleaseEvent
qDebug()<<"mouseReleaseEvent m_anchorBottomLeft";
break;
}
}
}
return QGraphicsRectItem::sceneEventFilter(watched, event);
}

QGraphicsScene::removeItem: item scene is different from this scene

There is something weird with QGraphicsScene::removeItem, when I remove two items from the scene I get the following message:
QGraphicsScene::removeItem: item 0x5edb28's scene (0x0) is different from this scene (0x5e7790)
QGraphicsScene::removeItem: item 0x5edc18's scene (0x0) is different from this scene (0x5e7790)
Even though the two items are in the scene.
I've checked the items scene against the scene but it returns true:
if(Player->scene() == m_scene){
m_scene->addText("Same"); // this text is added
}else{
m_scene->addText("Different");
}
m_scene come from this class:
class Game : public QGraphicsView
{
Q_OBJECT
public:
Game();
public slots:
void FixedUpdate();
signals:
private:
QRectF *sceneSize;
QGraphicsScene *m_scene;
QGraphicsRectItem *m_rect;
QTimer *m_timer;
Character *Player;
Character *enemy;
};
And the class Character is a class inheriting from QGraphicsPolygonItem:
class Character : public QObject, public QGraphicsPolygonItem
{
Q_OBJECT
public:
Character(qreal x, qreal y, qreal w, qreal h, QString tag, QGraphicsScene *scene = 0, QGraphicsItem *parent = 0);
Character(QRectF rectF, QString tag, QGraphicsScene *scene = 0, QGraphicsItem *parent = 0);
Character(QString tag, QGraphicsScene *scene = 0, QGraphicsItem *parent = 0);
void setValue(qreal x, qreal y, qreal w, qreal h);
void setVelocity(qreal vx, qreal vy);
void setVelocityX(qreal vx);
void setVelocityY(qreal vy);
void setTag(QString tag);
qreal vx() const;
qreal vy() const;
qreal w() const;
qreal h() const;
QString tag() const;
QGraphicsScene *currentScene() const;
void fixedUpdate();
void removeCharacter();
signals:
public slots:
private:
qreal m_x;
qreal m_y;
qreal m_w;
qreal m_h;
qreal m_vx;
qreal m_vy;
QString m_tag;
QGraphicsScene *m_scene;
};
And this is where I call removeItem
void Game::FixedUpdate(){
if(Player->collidesWithItem(enemy)){
Player->setVelocityX(0);
enemy->setVelocityX(0);
//removeCharacter just call scene()->removeItem(this);
Player->removeCharacter();
enemy->removeCharacter();
}else{
Player->fixedUpdate();
enemy->fixedUpdate();
}
}
Can someone explain me why I get these messages, please?
EDIT:
Game::Game(){
sceneSize = new QRectF(0, 0, 640, 480);
m_scene = new QGraphicsScene(*sceneSize, this);
setScene(m_scene);
Player = new Character(QRectF(0, 0, 50, 50), "player", m_scene);
Player->setVelocityX(1.5);
//m_scene->addItem(Player);
enemy = new Character(QRectF(500, 0, 20, 20), "enemy", m_scene);
enemy->setVelocityX(-0.5);
//m_scene->addItem(enemy);
//m_scene->addText(QString::number(Player->x() + Player->vx()));
if(Player->scene() == m_scene){
m_scene->addText("Same");
}else{
m_scene->addText("Different");
}
m_timer = new QTimer(this);
connect(m_timer, SIGNAL(timeout()), this, SLOT(FixedUpdate()));
m_timer->start(1000 / 60);
}
Finally I've found it.
It was in Game::FixedUpdate() function, it's Player->collidesWithItem(enemy) being true even after the pointers were removed from the scene. So I've put a check before it checks for collision, as so:
void Game::FixedUpdate(){
if(Player->scene() != NULL && enemy->scene() != NULL){
if(Player->collidesWithItem(enemy)){
Player->setVelocityX(0);
enemy->setVelocityX(0);
//removeCharacter just call scene()->removeItem(this);
Player->removeCharacter();
enemy->removeCharacter();
}else{
Player->fixedUpdate();
enemy->fixedUpdate();
}
}
}
Which does not give the messages after removal.

Draw a scale ruler in QGraphicsScene?

I am building a map widget (something like the google map) using Qt, basically I used a QGraphicsScene to display the map tile.
Now I want to add a scale ruler to the widget just like the one in google map.
Any suggestions about how could I realize this?
Take a look at this example:
Structure your code base as following:
Write a class inheriting descendants class of QAbstractScrollArea (As example QGraphicsView, QMdiArea, QPlainTextEdit, QScrollArea, QTextEdit, QColumnView, QHeaderView, QListView, QTableView, QTreeView etc.)
In the constructor of your class call setViewportMargins and set the margins of left/top/right/bottom areas length.
Create a QGridLayout and adds your custom Ruler/Scale in the layout.
Set this layout calling setLayout
Example:
setViewportMargins(RULER_BREADTH,RULER_BREADTH,0,0);
QGridLayout* gridLayout = new QGridLayout();
gridLayout->setSpacing(0);
gridLayout->setMargin(0);
mHorzRuler = new QDRuler(QDRuler::Horizontal);
mVertRuler = new QDRuler(QDRuler::Vertical);
QWidget* fake = new QWidget();
fake->setBackgroundRole(QPalette::Window);
fake->setFixedSize(RULER_BREADTH,RULER_BREADTH);
gridLayout->addWidget(fake,0,0);
gridLayout->addWidget(mHorzRuler,0,1);
gridLayout->addWidget(mVertRuler,1,0);
gridLayout->addWidget(this->viewport(),1,1);
this->setLayout(gridLayout);
QDRuler: The ruler class
#define RULER_BREADTH 20
class QDRuler : public QWidget
{
Q_OBJECT
Q_ENUMS(RulerType)
Q_PROPERTY(qreal origin READ origin WRITE setOrigin)
Q_PROPERTY(qreal rulerUnit READ rulerUnit WRITE setRulerUnit)
Q_PROPERTY(qreal rulerZoom READ rulerZoom WRITE setRulerZoom)
public:
enum RulerType { Horizontal, Vertical };
QDRuler(QDRuler::RulerType rulerType, QWidget* parent)
: QWidget(parent), mRulerType(rulerType), mOrigin(0.), mRulerUnit(1.),
mRulerZoom(1.), mMouseTracking(false), mDrawText(false)
{
setMouseTracking(true);
QFont txtFont("Goudy Old Style", 5,20);
txtFont.setStyleHint(QFont::TypeWriter,QFont::PreferOutline);
setFont(txtFont);
}
QSize minimumSizeHint() const
{
return QSize(RULER_BREADTH,RULER_BREADTH);
}
QDRuler::RulerType rulerType() const
{
return mRulerType;
}
qreal origin() const
{
return mOrigin;
}
qreal rulerUnit() const
{
return mRulerUnit;
}
qreal rulerZoom() const
{
return mRulerZoom;
}
public slots:
void setOrigin(const qreal origin)
{
if (mOrigin != origin)
{
mOrigin = origin;
update();
}
}
void setRulerUnit(const qreal rulerUnit)
{
if (mRulerUnit != rulerUnit)
{
mRulerUnit = rulerUnit;
update();
}
}
void setRulerZoom(const qreal rulerZoom)
{
if (mRulerZoom != rulerZoom)
{
mRulerZoom = rulerZoom;
update();
}
}
void setCursorPos(const QPoint cursorPos)
{
mCursorPos = this->mapFromGlobal(cursorPos);
mCursorPos += QPoint(RULER_BREADTH,RULER_BREADTH);
update();
}
void setMouseTrack(const bool track)
{
if (mMouseTracking != track)
{
mMouseTracking = track;
update();
}
}
protected:
void mouseMoveEvent(QMouseEvent* event)
{
mCursorPos = event->pos();
update();
QWidget::mouseMoveEvent(event);
}
void paintEvent(QPaintEvent* event)
{
QPainter painter(this);
painter.setRenderHints(QPainter::TextAntialiasing | QPainter::HighQualityAntialiasing);
QPen pen(Qt::black,0); // zero width pen is cosmetic pen
//pen.setCosmetic(true);
painter.setPen(pen);
// We want to work with floating point, so we are considering
// the rect as QRectF
QRectF rulerRect = this->rect();
// at first fill the rect
//painter.fillRect(rulerRect,QColor(220,200,180));
painter.fillRect(rulerRect,QColor(236,233,216));
// drawing a scale of 25
drawAScaleMeter(&painter,rulerRect,25,(Horizontal == mRulerType ? rulerRect.height()
: rulerRect.width())/2);
// drawing a scale of 50
drawAScaleMeter(&painter,rulerRect,50,(Horizontal == mRulerType ? rulerRect.height()
: rulerRect.width())/4);
// drawing a scale of 100
mDrawText = true;
drawAScaleMeter(&painter,rulerRect,100,0);
mDrawText = false;
// drawing the current mouse position indicator
painter.setOpacity(0.4);
drawMousePosTick(&painter);
painter.setOpacity(1.0);
// drawing no man's land between the ruler & view
QPointF starPt = Horizontal == mRulerType ? rulerRect.bottomLeft()
: rulerRect.topRight();
QPointF endPt = Horizontal == mRulerType ? rulerRect.bottomRight()
: rulerRect.bottomRight();
painter.setPen(QPen(Qt::black,2));
painter.drawLine(starPt,endPt);
}
private:
void drawAScaleMeter(QPainter* painter, QRectF rulerRect, qreal scaleMeter, qreal startPositoin)
{
// Flagging whether we are horizontal or vertical only to reduce
// to cheching many times
bool isHorzRuler = Horizontal == mRulerType;
scaleMeter = scaleMeter * mRulerUnit * mRulerZoom;
// Ruler rectangle starting mark
qreal rulerStartMark = isHorzRuler ? rulerRect.left() : rulerRect.top();
// Ruler rectangle ending mark
qreal rulerEndMark = isHorzRuler ? rulerRect.right() : rulerRect.bottom();
// Condition A # If origin point is between the start & end mard,
//we have to draw both from origin to left mark & origin to right mark.
// Condition B # If origin point is left of the start mark, we have to draw
// from origin to end mark.
// Condition C # If origin point is right of the end mark, we have to draw
// from origin to start mark.
if (mOrigin >= rulerStartMark && mOrigin <= rulerEndMark)
{
drawFromOriginTo(painter, rulerRect, mOrigin, rulerEndMark, 0, scaleMeter, startPositoin);
drawFromOriginTo(painter, rulerRect, mOrigin, rulerStartMark, 0, -scaleMeter, startPositoin);
}
else if (mOrigin < rulerStartMark)
{
int tickNo = int((rulerStartMark - mOrigin) / scaleMeter);
drawFromOriginTo(painter, rulerRect, mOrigin + scaleMeter * tickNo,
rulerEndMark, tickNo, scaleMeter, startPositoin);
}
else if (mOrigin > rulerEndMark)
{
int tickNo = int((mOrigin - rulerEndMark) / scaleMeter);
drawFromOriginTo(painter, rulerRect, mOrigin - scaleMeter * tickNo,
rulerStartMark, tickNo, -scaleMeter, startPositoin);
}
}
void drawFromOriginTo(QPainter* painter, QRectF rulerRect, qreal startMark, qreal endMark, int startTickNo, qreal step, qreal startPosition)
{
bool isHorzRuler = Horizontal == mRulerType;
int iterate = 0;
for (qreal current = startMark;
(step < 0 ? current >= endMark : current <= endMark); current += step)
{
qreal x1 = isHorzRuler ? current : rulerRect.left() + startPosition;
qreal y1 = isHorzRuler ? rulerRect.top() + startPosition : current;
qreal x2 = isHorzRuler ? current : rulerRect.right();
qreal y2 = isHorzRuler ? rulerRect.bottom() : current;
painter->drawLine(QLineF(x1,y1,x2,y2));
if (mDrawText)
{
QPainterPath txtPath;
txtPath.addText(x1 + 1,y1 + (isHorzRuler ? 7 : -2),this->font(),QString::number(qAbs(int(step) * startTickNo++)));
painter->drawPath(txtPath);
iterate++;
}
}
}
void drawMousePosTick(QPainter* painter)
{
if (mMouseTracking)
{
QPoint starPt = mCursorPos;
QPoint endPt;
if (Horizontal == mRulerType)
{
starPt.setY(this->rect().top());
endPt.setX(starPt.x());
endPt.setY(this->rect().bottom());
}
else
{
starPt.setX(this->rect().left());
endPt.setX(this->rect().right());
endPt.setY(starPt.y());
}
painter->drawLine(starPt,endPt);
}
}
private:
RulerType mRulerType;
qreal mOrigin;
qreal mRulerUnit;
qreal mRulerZoom;
QPoint mCursorPos;
bool mMouseTracking;
bool mDrawText;
};

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:
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:
Store the line angle.
Set the angle to 0.
Copy the line.
Use QLineF::translate() on the copy.
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.
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!
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);
}
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