Qt: Overlapping semitransparent QgraphicsItem - c++

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);

Related

QPixmap paint performance issue

I need to recreate the Windows 7 theme where the application window header is transparent and displays blurred screen contents. My idea was to capture the screen contents and display them blurred in the header. For that reason I extended QQuickPaintedItem.
Here is the header:
class DesktopImage : public QQuickPaintedItem
{
Q_OBJECT
Q_PROPERTY(int desktopX READ desktopX WRITE setDesktopX NOTIFY desktopXChanged)
Q_PROPERTY(int desktopY READ desktopY WRITE setDesktopY NOTIFY desktopYChanged)
public:
explicit DesktopImage(QQuickItem *parent = nullptr);
void paint(QPainter *painter) override;
int desktopX() const;
void setDesktopX(int desktopX);
int desktopY() const;
void setDesktopY(int desktopY);
signals:
void desktopXChanged();
void desktopYChanged();
private:
void grabScreensContent();
private:
QPixmap mScreensContent;
int mDesktopX;
int mDesktopY;
};
the grabScreensContent() method do as the name suggest. The paint() method is implemented as follows:
void DesktopImage::paint(QPainter *painter)
{
QRectF target(0, 0, width(), height());
QRectF source(mDesktopX, mDesktopY, width(), height());
painter->drawPixmap(target, mScreensContent, source);
}
on the QML side, I use the type as follows:
DesktopContent {
id: desktop
desktopX: window.x
desktopY: window.y
width: parent.width
height: parent.height
}
as you can see the desktopX (desktopY) properties are bound to the window x (window y) properties so that when the user moves the window the portion of the background that needs to be drawn is properly fetched. However the drawing is not as fluid as one might expect. Here is the result:
Can someone suggest an performance improvement?
Take care of renderTarget by setting it to FramebufferObject. This basically should make it as efficient as normal QML rendering yet you can use QPainter which is sometimes conveninent.
DesktopImage::DesktopImage(QQuickItem *parent)
{
// this setting is not default
this->setRenderTarget(QQuickPaintedItem::FramebufferObject);
}
Also, only operate with the limited screen area if you don't need the whole screen. This may or may not help depending on the platform / implementation but I always limit the scope on the target first and then position the source within it. The low level painting is not necessarily very intelligent and may expect many changes across the whole area. So we should rather specify the minimum target area and just alter it (this case works like that).
void DesktopImage::paint(QPainter *painter)
{
QRectF target(mDesktopX, mDesktopY, width(), height()); // now limited
QRectF source(0, 0, width(), height()); // now within smaller target
painter->drawPixmap(target, mScreensContent, source);
}

Qt animate ellipses along the circle

I am new to the Qt and I am confused how to use the QGraphicsScene. If I add for example 10 ellipses to the scene and animate them along the path, how would I do it? It gets even more complicated, because if I have 10 ellipses drawn along the ellipse and I want to animate those ellipses so they move away from the cente of the ellipse they are on. In the picture you can see the ellipses. Those are drawn with QPainter I haven't figured out how to add them to scene, but I would like the grey ellipses to move in between the inner and outter circle. I have gone through some examples, but can't really fit any of those to my situation.
Code used to draw ellipses with QPainter:
QPainter drElps;
drElps.setBrush(QBrush(QColor(0xaf, 0xaf, 0xaa)));
drElps.setPen(QPen(QColor(0xaf, 0xaf, 0xaa), 2));
double nrElps = 30;
int degree = (int)(360./nrElps);
painter.translate(QPointF(big_elps_circle_center));
while(--nrElps){
painter.drawEllipse(0, -60, 3, 3);
painter.rotate(degree);
}
painter.translate(QPOintF(back_to_origin));
Qt has a set of classes oriented to the animations, for this you must first create an object that inherits from QGraphicsObject, in this class you must implement the methods paint and boundingRect.
ellipseobject.h
#ifndef ELLIPSEOBJECT_H
#define ELLIPSEOBJECT_H
#include <QGraphicsObject>
class EllipseObject : public QGraphicsObject
{
Q_OBJECT
public:
EllipseObject(QGraphicsItem * parent = 0);
void paint(QPainter * painter, const QStyleOptionGraphicsItem * option, QWidget * widget = 0);
QRectF boundingRect() const;
};
#endif // ELLIPSEOBJECT_H
ellipseobject.cpp
#include "ellipseobject.h"
#include <QPainter>
EllipseObject::EllipseObject(QGraphicsItem *parent):QGraphicsObject(parent)
{
}
void EllipseObject::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
Q_UNUSED(option)
Q_UNUSED(widget)
painter->setRenderHint(QPainter::Antialiasing);
painter->setPen(QColor(0xaf, 0xaf, 0xaa));
painter->setBrush(QBrush(QColor(0xaf, 0xaf, 0xaa)));
QRectF rect = boundingRect();
painter->drawEllipse(rect);
}
QRectF EllipseObject::boundingRect() const
{
return QRectF(-4, -4, 8, 8);
}
Then we can use the QPropertyAnimation class, with this we will animate the position.
EllipseObject *item = new EllipseObject;
QPropertyAnimation *animation = new QPropertyAnimation(item, "pos");
for(int j = 0; j < p; j++){
animation->setKeyValueAt( 1.0*j/(p-1),
(r+ delta*sin(2*M_PI*j/p) )*QPointF(qSin(2*M_PI*i/number_of_items), qCos(2*M_PI*i/number_of_items)));
}
animation->setDuration(2000);
Since we have several elements that are animated in parallel, we can use QParallelAnimationGroup to handle each of the animations.
group = new QParallelAnimationGroup(this);
[...]
group->addAnimation(animation);
If we want to be continuous we can make the following code.
group->start();
connect(group, &QParallelAnimationGroup::finished,[=](){
group->start();
});
The complete code is here.
If you draw them with QPainter then you are on your on. The point of QGraphivsScene is to inherit from QGraphicsItem, paint what you need to paint on the paintEvent, add them to the scene.
you are drawint more than 1 item on your paintEvent - so you are on your own - and QGraphivsScene cannot help you there.
Now, how to do that Correctly:
Inherit from QGraphicsScene or QGraphivsView
Inherit from QGraphicsEllipseItem and create a MovableEllipseItem
in the MovableEllipseItem constructor, create a QTimer, connect it's timeout() signal with a move slot that you will define
on your move slot, do the calculations on where the ellipse should be and move(x,y) it.
You can use a custom Graphics Item, the QGraphicsScene::advance mechanism, together with a QTimeLine to control the animation.
The outline is like this:
QGraphicsScene *sc = new QGraphicsScene;
QTimeLine *tl = new QTimeLine(1000);
tl->setFrameRange(-20, 20);
connect(tl, &QTimeLine::frameChanged, sc, &QGraphicsScene::advance);
connect(tl, &QTimeLine::finished, tl, &QTimeLine::toggleDirection);
connect(tl, &QTimeLine::finished, tl, &QTimeLine::start);
for (int i = 0; i < 30; ++i) {
sc->addItem(new AnimItem(360./30*i, tl));
}
In the custom AnimItem, you implement the drawing/animation logic. A good base would be QGraphicsEllipseItem. For example:
AnimItem::AnimItem(qreal angle, QTimeLine *timer, QGraphicsItem *parent)
: QGraphicsEllipseItem(0, 0, 3, 3, parent),
m_timer(timer)
{
QTransform t;
t.rotate(angle);
t.translate(0, -120);
setTransform(t);
setBrush(QBrush(QColor(0xaf, 0xaf, 0xaa)));
}
void AnimItem::advance(int phase)
{
if (phase == 1) {
QTransform t = transform();
t.translate(0, m_timer->currentFrame()/5);
setTransform(t);
}
}

QT 5.7 QPainter line aligment

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"

Qt "mousePressEvent" modify the clickable area

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.

QPainter::drawImage() clips QImage when X, Y are not 0 on a QDeclarativeItem

I'm developing a new QML element in C++ based on this sample code. My class inherits from QDeclarativeItem and it's paint() method is responsible to draw a QImage to the screen:
void NewQMLitem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
// image is QImage* that was loaded at the constructor of the class
painter->drawImage(0, 0, *image);
}
The widget size is 800x480 and the image is 400x240. The code below works perfectly as long as the drawing starts at (0, 0), as you can see below:
The problem I'm facing is that drawing at an arbitrary coordinate such as(200, 0), seems to clip the drawing. It looks like QPainter only updates the screen starting from (0, 0):
void NewQMLitem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
painter->drawImage(200, 0, *image);
}
And the following is the result:
My current workaround involves calling widget->update() at the end of the paint(). Of course, this is terrible because it makes the widget be painted twice.
What is the proper way to handle this? I'm currently using Qt 5.2 on Windows.
I found out that in these situations you need to call prepareGeometryChange() to let the parent know that the size/position of your widget has changed, and that it needs to query the new geometry to be able to paint it correctly to the screen.
This will make the parent invoke boundingRect() on your widget, so it's your responsibility to implement it:
void NewQMLitem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
prepareGeometryChange()
painter->drawImage(200, 0, *image);
}
QRectF NewQMLitem::boundingRect() const
{
return QRectF(m_x, m_y, m_width, m_height);
}