I have a question about drawing specific arc on a scene. I have this information about arc:
Starting Koordinates,
Start Angle,
End Angle ,
Radius.
But I can't use them efficently with QPainter. Actually I tried QPainterPath to use shape to show on QGraphicsScene with addPath("") but I can't use function properly. My questions are about how to use this infortmation to draw arc and how to show it on my graphic scene.
You can use a QGraphicsEllipseItem to add ellipses, circles, and segments/arcs to a QGraphicsScene.
Try
QGraphicsEllipseItem* item = new QGraphicsEllipseItem(x, y, width, height);
item->setStartAngle(startAngle);
item->setSpanAngle(endAngle - startAngle);
scene->addItem(item);
Unfortunately, QGraphicsEllipseItem only supports QPainter::drawEllipse() and QPainter::drawPie() - the latter can be used to draw arcs, but has the side effect that there is always a line drawn from the start and the end of the arc to the center.
If you require a true arc, you can e.g. subclass QGraphicsEllipseItem and override the paint() method:
class QGraphicsArcItem : public QGraphicsEllipseItem {
public:
QGraphicsArcItem ( qreal x, qreal y, qreal width, qreal height, QGraphicsItem * parent = 0 ) :
QGraphicsEllipseItem(x, y, width, height, parent) {
}
protected:
void paint ( QPainter * painter, const QStyleOptionGraphicsItem * option, QWidget * widget) {
painter->setPen(pen());
painter->setBrush(brush());
painter->drawArc(rect(), startAngle(), spanAngle());
// if (option->state & QStyle::State_Selected)
// qt_graphicsItem_highlightSelected(this, painter, option);
}
};
You then still need to handle the item highlighting, unfortunately qt_graphicsItem_highlightSelected is a static function defined inside the Qt library.
Related
I have some QGraphicsItems in the QGraphicsScene which should keep the same size and position when scaling. I've tried QGraphicsItem::ItemIgnoresTransformations but it turns out that the items get wrong positions. Below is a sample code:
I have subclassed QGraphicsView like this:
class Graphics : public QGraphicsView
{
public:
Graphics();
QGraphicsScene *scene;
QGraphicsRectItem *rect;
QGraphicsRectItem *rect2;
protected:
void wheelEvent(QWheelEvent *event);
};
And in its constructor:
Graphics::Graphics()
{
scene = new QGraphicsScene;
rect = new QGraphicsRectItem(100,100,50,50);
rect2 = new QGraphicsRectItem(-100,-100,50,50);
scene->addLine(0,200,200,0);
rect->setFlag(QGraphicsItem::ItemIgnoresTransformations, true);
scene->addItem(rect);
scene->addItem(rect2);
setScene(scene);
scene->addRect(scene->itemsBoundingRect());
}
The wheelEvent virtual function:
void Graphics::wheelEvent(QWheelEvent *event)
{
if(event->delta() < 0)
scale(1.0/2.0, 1.0/2.0);
else
scale(2, 2);
scene->addRect(scene->itemsBoundingRect());
qDebug() << rect->transform();
qDebug() << rect->boundingRect();
qDebug() << rect2->transform();
qDebug() << rect2->boundingRect();
}
orginal view looks like this:
1
take the line as road and rect aside as a symbol. When zoomed out, the rect maintain its size but jumps out of the scene:
2
which should be that topleft of rect to middle of line. I'm also confused with debug info showing that the boundingRect and transform stays the same, which seems that nothing has changed! What causes the problem and is there any way to solve it? Could someone help? Thank you!
Sorry for delay, now I've solved the problem myself.
I found QGraphicsItem::ItemIgnoresTransformations only works when the point you want stick to is at (0,0) in item's coordinate. You need also update boundingRect manually in this way. Nevertheless, the best solution I've found is subclass QGraphicsItem and set matrix in paint() according to world matrix. Below is my code .
QMatrix stableMatrix(const QMatrix &matrix, const QPointF &p)
{
QMatrix newMatrix = matrix;
qreal scaleX, scaleY;
scaleX = newMatrix.m11();
scaleY = newMatrix.m22();
newMatrix.scale(1.0/scaleX, 1.0/scaleY);
qreal offsetX, offsetY;
offsetX = p.x()*(scaleX-1.0);
offsetY = p.y()*(scaleY-1.0);
newMatrix.translate(offsetX, offsetY);
return newMatrix;
}
And the paint function:
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option,
QWidget *widget)
{
QPointF p(left, top);
painter->setMatrix(stableMatrix(painter->worldMatrix(), p));
painter->drawRect(left, top, width, height);
}
The second argument of stableMatrix is sticked point, in my sample code it's top-left of the item. You can change it to your preference. It works really fine!
Hope this post help :)
The solution to this is even simpler.
QGraphicsItem::ItemIgnoresTransformations
The item ignores inherited transformations (i.e., its position is still anchored to its parent, but the parent or view rotation, zoom or shear transformations are ignored). [...]
And that's the key! Item ignores all transformations, but is still bound to its parent. So you need two items: a parent item that will keep the relative position (without any flags set) and a child item that will do the drawing (with QGraphicsItem::ItemIgnoresTransformations flag set) at parent's (0,0) point.
Here is some working code of a crosshair that have constant size and rotation, while keeping the relative position to its parent:
#include <QGraphicsItem>
#include <QPainter>
class CrossHair : public QGraphicsItem
{
private:
class CrossHairImpl : public QGraphicsItem
{
public:
CrossHairImpl (qreal len, QGraphicsItem *parent = nullptr)
: QGraphicsItem(parent), m_len(len)
{
setFlag(QGraphicsItem::ItemIgnoresTransformations);
}
QRectF boundingRect (void) const override
{
return QRectF(-m_len, -m_len, m_len*2, m_len*2);
}
void paint (QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) override
{
painter->setPen(QPen(Qt::red, 1));
painter->drawLine(0, -m_len, 0, m_len);
painter->drawLine(-m_len, 0, m_len, 0);
}
private:
qreal m_len;
};
public:
CrossHair (qreal x, qreal y, qreal len, QGraphicsItem *parent = nullptr)
: QGraphicsItem(parent), m_impl(len, this) // <-- IMPORTANT!!!
{
setPos(x, y);
}
QRectF boundingRect (void) const override
{
return QRectF();
}
void paint (QPainter *, const QStyleOptionGraphicsItem *, QWidget *) override
{
// empty
}
private:
CrossHairImpl m_impl;
};
I am working with QT 5.7 and C++.
At the moment I try to get used to draw my own widgets with the QPainter class.
But I noticed a problem I couldn't solve.
I try to draw a border line extactly at the widget border but if I do so:
void MyWidget::paintEvent(QPaintEvent *event)
{
QPainter painter;
painter.begin(this);
painter.setBrush(Qt::cyan);
QBrush brush(Qt::black);
QPen pen(brush, 2);
painter.setPen(pen);
painter.drawRect(0, 0, size().width() - 1, size().height() - 1);
painter.end();
}
The Line is at the bottom and right site bigger than the others:
And before someone is telling me I have to remove the two -1 expressions,
you should know if I do this and also set the pen width to 1 there is no line anymore at the bottom and right side.
I think this artifact is caused by the "line aligment".
QT tries to tint the the pixels near the logical lines defined by the rectangle but actually because finally all have to be in pixels it has to decide.
If I am right, why there is no method to set the line aligment of the pen like in GDI+?
And how I can solve this?
Everything depends on whether you want the entire pen's width to be visible or not. By drawing the rectangle starting at 0,0, you're only showing half of the pen's width, and that makes things unnecessarily complicated - never mind that the line appears too thin. In Qt, the non-cosmetic pen is always drawn aligned to the middle of the line. Qt doesn't let you change it: you can change the drawn geometry instead.
To get it right for odd line sizes, you must give rectangle's coordinates as floating point values, and they must be fall in the middle of the line. So, e.g. if the pen is 3.0 units wide, the rectangle's geometry will be (1.5, 1.5, width()-3.0, width()-3.0).
Here's a complete example:
// https://github.com/KubaO/stackoverflown/tree/master/questions/widget-pen-wide-38019846
#include <QtWidgets>
class Widget : public QWidget {
Q_OBJECT
Q_PROPERTY(qreal penWidth READ penWidth WRITE setPenWidth)
qreal m_penWidth = 1.0;
protected:
void paintEvent(QPaintEvent *) override {
QPainter p{this};
p.setPen({Qt::black, m_penWidth, Qt::SolidLine, Qt::SquareCap, Qt::MiterJoin});
p.setBrush(Qt::cyan);
qreal d = m_penWidth/2.0;
p.drawRect(QRectF{d, d, width()-m_penWidth, height()-m_penWidth});
}
public:
explicit Widget(QWidget * parent = 0) : QWidget{parent} { }
qreal penWidth() const { return m_penWidth; }
void setPenWidth(qreal width) {
if (width == m_penWidth) return;
m_penWidth = width;
update();
}
QSize sizeHint() const override { return {100, 100}; }
};
int main(int argc, char ** argv) {
QApplication app{argc, argv};
QWidget top;
QVBoxLayout layout{&top};
Widget widget;
QSlider slider{Qt::Horizontal};
layout.addWidget(&widget);
layout.addWidget(&slider);
slider.setMinimum(100);
slider.setMaximum(1000);
QObject::connect(&slider, &QSlider::valueChanged, [&](int val){
widget.setPenWidth(val/100.0);
});
top.show();
return app.exec();
}
#include "main.moc"
I've been working with QGraphicsView for a while and I am facing a requisite that I am not sure if it can be fulfilled by using this framework.
Putting it as simple as possible, I have 2 overlapping RectItem with a semitransparent QBrush (the same one for both). Is it possible to prevent the overlapping area from becoming more opaque? I just want the whole area to have the same color (this will occur only if both rects are fully opaque, but sometimes that is not the case)
I know it might seem a weird requisite, but the old graphics engine my colleagues used allowed it.
Any ideas?
Qt provides various blend (composition) modes for the QPainter. Deriving your RectItem class from QGraphicsItem or QGraphicsObject, allows you to customise the painting and using the composition modes, create various effects, as demonstrated in the Qt Example.
If you want two semi-transparent items overlapping without changing the colour (assuming their colour is the same), either the QPainter::CompositionMode_Difference mode, or CompositionMode_Exclusion will do this. Here's example code of such an object: -
Header
#ifndef RECTITEM_H
#define RECTITEM_H
#include <QGraphicsItem>
#include <QColor>
class RectItem : public QGraphicsItem
{
public:
RectItem(int width, int height, QColor colour);
~RectItem();
QRectF boundingRect() const;
private:
QRectF m_boundingRect;
QColor m_colour;
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = 0);
};
#endif // RECTITEM_H
Implementation
#include "rectitem.h"
#include <QPainter>
RectItem::RectItem(int width, int height, QColor colour)
: QGraphicsItem(), m_boundingRect(-width/2, -height/2, width, height), m_colour(colour)
{
setFlag(QGraphicsItem::ItemIsSelectable);
setFlag(QGraphicsItem::ItemIsMovable);
}
RectItem::~RectItem()
{
}
QRectF RectItem::boundingRect() const
{
return m_boundingRect;
}
void RectItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *)
{
painter->setCompositionMode(QPainter::CompositionMode_Difference);
painter->setBrush(m_colour);
painter->drawRect(m_boundingRect);
}
You can now create two RectItem objects of the same semi-transparent colour and add them to the scene
// assuming the scene and view are setup and m_pScene is a pointer to the scene
RectItem* pItem = new RectItem(50, 50, QColor(255, 0, 0, 128));
pItem->setPos(10, 10);
m_pScene->addItem(pItem);
pItem = new RectItem(50, 50, QColor(255, 0, 0, 128));
pItem->setPos(80, 80);
m_pScene->addItem(pItem);
I am having a problem with mousePressEvent(QGraphicsSceneMouseEvent *event), indeed the clickable area seems small and off centre to the QGraphicsPixmapItem it is linked to.
The red line is where the QGraphicsPixmapItem is clickable.
How would I centre it and eventually make it bigger and change it's shape ?
Here are the portions of my code that can be useful :
In player.h
class Player:public QObject, public QGraphicsPixmapItem{
Q_OBJECT
public:
Player();
void place_player(int x, int y);
void mousePressEvent(QGraphicsSceneMouseEvent *event);
};
In player.cpp
Player::Player(): QGraphicsPixmapItem(){
}
void Player::place_player(int x,int y)
{
this->setPixmap(QPixmap("test.png"));
this->setPos(x,y);
game->scene->addItem(this);
}
void Player::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
qDebug()<< event;
};
in game.cpp
Game::Game(){
setFixedSize(1600,900);
scene = new QGraphicsScene(this);
scene->setSceneRect(0,0,1600,900);
setScene(scene);
}
void Game::start(){
player1 = new Player();
player1->place_player(300,300);
}
void Game::mousePressEvent(QMouseEvent *event)
{
QGraphicsView::mousePressEvent(event);
}
And finally the main.cpp
int main(int argc, char *argv[]){
QApplication a(argc, argv);
game = new Game();
game->show();
game->start();
return a.exec();
}
Thanks a lot for your help
The clickable area of a QGraphicsItem is defined by its boundingRect and shape functions.
I would start by not using QGraphicsPixmapItem. You want a custom graphics item, which has the functionality of signals and slots, so derive from QGraphicsObject.
class Player : public QGraphicsObject
{
};
As we've now derived from this class, we need to override a couple of pure, virtual functions; namely boundingRect, paint
class Player : public QGraphicsObject
{
public:
QRectF boundingRect() const;
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option,QWidget *widget);
};
The boundingRect function defines the object in local coordinates. For an example, let's assume the character will have a width and height of 100. If we set the boundingRect to return (0, 0, 100, 100), this would be oriented about the top left corner. Instead, we want to centre the bounding rect on our Player:
QRectF Player::boundingRect() const
{
return QRectF(-50, -50, 100, 100); // local coordinates, centered on the Player
}
To draw our Player, store a QPixmap in the class
class Player : public QGraphicsObject
{
public:
QRectF boundingRect() const;
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option,QWidget *widget);
private:
QPixmap m_playerPixmap;
};
I'll assume you know how to load the pixmap and can do that in the constructor of the player.
All we need now is to render the player and we'll also show the clickable area, which is defined by the boundingRect() function: -
void Player::paint(QPainter *painter, const QStyleOptionGraphicsItem *option,QWidget *widget)
{
// draw the player
painter->drawPixmap(0, 0, m_playerPixmap);
// set the pen to draw debug rect
painter->setPen(QColor(255, 0, 0, 127));
// for debug purposes, show the bounding rect (clickable area)
painter->drawRect(boundingRect());
}
Originally I mentioned that the clickable area is defined by the boundingRect and shape functions. As the Player is of uniform shape (a rectangle), we only care about the boundingRect. In the case of an irregular shape, you would also override the shape function.
How would I centre it and eventually make it bigger and change it's shape ?
Hopefully you now know that to make the Player bigger, it's just a matter of increasing its local coordinates returned in the boundingRect function. So, if we want to double its width and height, we'd do this:
QRectF Player::boundingRect() const
{
return QRectF(-100, -100, 200, 200); // local coordinates, centered on the Player
}
To change its shape, implement the shape() function and to debug, paint the painterPath returned from that function, instead of drawing the boundingRect.
For example, let's have a circular, clickable area.
Assuming you've added the shape declaration to the Player header:
QPainterPath Player::shape() const
{
QPainterPath path;
path.addEllipse(-100, -100, 200, 200);
return path;
}
void Player::paint(QPainter * painter, const QStyleOptionGraphicsItem, QWidget*)
{
// draw the player
painter->drawPixmap(0, 0, m_playerPixmap);
// set the pen to draw debug path
painter->setPen(QColor(255, 0, 0, 127));
// for debug purposes, show the path (clickable area)
painter->drawPath(shape());
}
One final thing to note is that if you're overriding the shape function, you must still implement boundingRect.
I am using Qt´s QGraphicsView - and QGraphicsItem-subclasses.
is there a way to not scale the graphical representation of the item in the view when the view rectangle is changed, e.g. when zooming in. The default behavior is that my items scale in relation to my view rectangle.
I would like to visualize 2d points which should be represented by a thin rectangle which should not scale when zooming in the view. See a typical 3d modelling software for reference where vertex points are always shown at the same size.
Thanks!
Set the QGraphicItem's flag QGraphicsItem::ItemIgnoresTransformations to true does not work for you?
I got into the same problem, and it took me a while to figure it out. This is how I solved it.
Extend a QGraphicsItem class, override paint().
Inside the paint(), reset the transformation's scaling factor to 1(which are m11 and m22), and save the m11(x scaling factor) and m22(y scaling factor) before the reset.
Then, draw like you would normally do but multiply your x with m11 and y with m22. This avoids drawing with the default transformation, but explicitly calculates the positions according to the scene's transformation.
void MyItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *item, QWidget *widget)
{
QTransform t = painter->transform();
qreal m11 = t.m11(), m22 = t.m22();
painter->save(); // save painter state
painter->setTransform(QTransform(1, t.m12(), t.m13(),
t.m21(), 1, t.m23(), t.m31(),
t.m32(), t.m33()));
int x = 0, y = 0; // item's coordinates
painter->drawText(x*m11, y*m22, "Text"); // the text itself will not be scaled, but when the scene is transformed, this text will still anchor correctly
painter->restore(); // restore painter state
}
The following code block is drawing with default transformation
void MyItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *item, QWidget *widget)
{
int x = 0, y = 0;
painter->drawText(x, y, "Text");
}
You can try both to see the difference. Hope this helps.
How about this:
#include <QtGui/QApplication>
#include <QtGui/QGraphicsScene>
#include <QtGui/QGraphicsView>
#include <QtGui/QGraphicsRectItem>
int main(int argc, char* argv[]) {
QApplication app(argc, argv);
QGraphicsScene scene;
scene.addText("Hello, world!");
QRect rect(50, 50, 100, 100);
QGraphicsRectItem* recti = scene.addRect(rect);
QGraphicsView view(&scene);
// Set scale for the view
view.scale(10.0, 5.0);
// Set the inverse transformation for the item
recti->setTransform(view.transform().inverted());
view.show();
return app.exec();
}
As you can see the text is scaled up but the rectangle is not. Note that this does not only prevent the scaling for the rectangle but and other transformation.
The following solution worked perfectly for me:
void MyDerivedQGraphicsItem::paint(QPainter *painter, const StyleOptionGraphicsItem *option, QWidget *widget)
{
double scaleValue = scale()/painter->transform().m11();
painter->save();
painter->scale(scaleValue, scaleValue);
painter->drawText(...);
painter->restore();
...
}
We can also multiply the scaleValue by other mesures we want to keep its size constant outside the save/restore environment.
QPointF ref(500, 500);
QPointF vector = scaleValue * QPointF(100, 100);
painter->drawLine(ref+vector, ref-vector);
I found that if I derive a new class and reimpliment the paint function I can do
void MyDerivedQGraphicsItem::paint(QPainter *painter,
const QStyleOptionGraphicsItem *option,
QWidget *widget)
{
double scaleValue = scale();
double scaleX = painter->transform().m11();
setScale(scaleValue / scaleX);
QGraphicsSvgItem::paint(painter,option,widget);
}
This is the best way of doing it that I have found so far, but I am still tinkering around.