Qt - Adding items to QGraphicsScene with absolute position - c++

This is probably a dump question but I'm really stuck with it right now. I'm trying to draw some squares in a QGraphicsScene and I want them to be aligned from position x = 0 towards the positive position of x coordinates. However they are aligned according to the alignment configuration of the QGraphicsView and the setting of position is only effective for the second item and upwards relative to the first item! This means that if I have a single item, then setting of it's position has no effect. Mainly this line seems not to be working:
graphicsView->setAlignment(Qt::AlignAbsolute);
This is my code:
QGraphicsScene *scene = new QGraphicsScene();
QGraphicsView *graphicsView;
graphicsView->setScene(scene);
graphicsView->setAlignment(Qt::AlignAbsolute);
for(int i = 0; i < 500; i+= 50)
{
QGraphicsPolygonItem *item = new QGraphicsPolygonItem();
item->setPen(QPen(QColor(qrand()%255,qrand()%255,qrand()%255)));
item->setBrush(QBrush(QColor(255,251,253)));
item->setPolygon(*myPolygon);
graphicsView->scene()->addItem(item);
item->setPos(i , 40);
item->setFlag(QGraphicsItem::ItemIsMovable, true);
item->setFlag(QGraphicsItem::ItemIsSelectable, true);
graphicsView->show();
}
I do not know what the problem might be, so I tried the following code
const QRectF rect = QRectF(0, 0, ui->graphicsView->width(), ui->graphicsView->height());
ui->graphicsView->setScene(&_scene);
ui->graphicsView->fitInView(rect, Qt::KeepAspectRatio);
ui->graphicsView->setSceneRect(rect);
With respect to the previous four lines, the following output does not produce sizes even close to each other:
qDebug() << "scene =>" << _scene.width() << _scene.height();
qDebug() << "graphicview =>" << ui->graphicsView->width() << ui->graphicsView->height();
I highly appreciate your help.

It seems that Qt::AlignAbsolute does not do what you assume it does. What you actually need is Qt::AlignLeft | Qt::AlignTop.
It is explained here:
http://qt-project.org/doc/qt-4.8/qgraphicsview.html#alignment-prop
http://qt-project.org/doc/qt-4.8/qt.html#AlignmentFlag-enum
In my code, I only use graphicsView->setSceneRect(rect); and not fitInView(). The latter introduces a scaling and may hinder your understanding on what is going wrong.
Basically, I overwrite the QGraphicsview to re-implement resizeEvent():
void AutohideView::resizeEvent(QResizeEvent *event)
{
/* always resize the scene accordingly */
if (scene())
scene()->setSceneRect(QRect(QPoint(0, 0), event->size()));
QGraphicsView::resizeEvent(event);
}
I do not alter alignment or any other options and use absolute coordinates in the range [0,0 .. scene()->width(),scene()->height()] when positioning my items.

Related

Qt5 C++ resize font based on label width

I have written what I think is a pretty good font resize algorithm that I use on labels in a QGridLayout. The algorithm works rather well on its own. However, the first time I run the algorithm, the layout hasn't been painted yet, and as such, the grid widgets which own the labels haven't been sized to the layout, which means my width() call is wrong, though I have assigned the label text prior to the resize (which doesn't matter). My question is, what is the best way to know the bounding rectangles have been created to fit the layout so my calculation is on the actual label width?
Note, this is Qt5.7, so I know that fm.width() is obsolete, but that's what's available in Raspbian.
My app is reading Sonos metadata and showing it on a 7" RPi display. I'm resizing the metadata that is too long to fit in the layout grids. So, I have a QStackedLayout with a set of layouts to show (clock when Sonos isn't playing, metadata when it is), and I frequently poll the a local Sonos server to find out if there is metadata to show. This works well. What doesn't happen is that on the first time the Sonos metadata layout is shown, the QGridLayout hasn't actually been laid out yet (I believe), so the labels are too big. At some point after the first time I fill in the metadata, the grid layout gets "shown" and the labels are then the correct size. The problem is, by then, it's all done setting metadata and the labels look funny in some cases.
int pointSize = FontSize::Default;
for (auto label : m_labels) {
QFont f = label->font();
if (label->accessibleName() == "title")
pointSize = FontSize::Title;
QFontMetrics fm(f);
if (fm.width(label->text()) > label->width()) {
float factor = (float)label->width() / (float)fm.width(label->text());
if (factor <= .6) {
factor = .6;
}
f.setPointSizeF((f.pointSize() * factor) * .9);
qDebug() << __FUNCTION__ << ": label width:" << label->width();
qDebug() << __FUNCTION__ << ": font width:" << fm.width(label->text());
qDebug() << __FUNCTION__ << ": Calculated font scaling factor to be" << (float)(factor * .9);
qDebug() << __FUNCTION__ << ": Reset font size for text\"" << label->text() << "\" to" << f.pointSize();
}
else
f.setPointSize(pointSize);
label->setFont(f);
}
On an 800x480 display, this results in the first event label width being wrong
calculateLabelFontSize : label width: 640
calculateLabelFontSize : font width: 2051
The label width the next time it's called would end up being correct with a width of 584. However, because I don't always call it a second time, and because sometimes, the Sonos metadata hiccups and causes the display to revert, it may always be wrong, or it may just be right the first time no matter what.
I've considered trying to overload my gui app paintEvent(QPaintEvent *event), but the QPaintEvent class doesn't give me a way to determine which widget called for the event, so I can't just ignore the event until I want it. At least, not that I have determined, so if that's possible, please let me know.
I've tried overloading showEvent, but that doesn't run except when the frame is first shown, so no help there.
I've considered subclassing QGridLayout to simply overload the paintEvent() and use that, but I do have an issue with that solution. There is a progress bar in this layout, which means that the paintEvent is going to fire every half second as I get metadata from the Sonos server. Since I don't know if the paintEvent is firing for the time update, or for the text being set, I get a lot of resize attempts.
I've also simply run the algorithm a second time on the next event, but it's a really weird looking graphical hiccup when the fonts realign, and I don't like it.
Last up, I may just subclass QLabel, simply to overload paintEvent, and use that custom label just for the labels that would be resizable. However, is the label in the layout painted before or after the container it lives in inside the layout? I'll be testing this today I think, but I'm not convinced it would work.
I figure this has a much easier solution though. I just can't figure it out.
I borrowed code from https://github.com/jonaias/DynamicFontSizeWidgets which provided the resize algorithm. It's nice, but iterative. I have been tinkering with a math based solution to calculate instead of just trying to fit by resizing until it works. However, that solution still doesn't work well enough to use. This isn't slow, and does what is implied. It resizes the font until it fits.
This all is done in paintEvent() because using resizeEvent() may result in either an infinite resize loop or the layout may resize itself and suddenly everything looks shifted/squished/stretched. Doing it in paintEvent means it's only called once every time you update the text, but not otherwise.
I may come back to this to try to find a math based solution. I still think this isn't the correct way, but I found something that worked well enough for now.
NewLabel.h
#ifndef NEWLABEL_H
#define NEWLABEL_H
#include <QtCore/QtCore>
#include <QtWidgets/QtWidgets>
#include <cmath>
class NewLabel : public QLabel
{
Q_OBJECT
public:
explicit NewLabel(const QString &text, QWidget *parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags());
explicit NewLabel(QWidget *parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags());
virtual ~NewLabel() {}
void setDefaultPointSize(int p)
{
m_defaultPointSize = static_cast<float>(p);
}
protected:
void paintEvent(QPaintEvent *e) override;
private:
float getWidgetMaximumFontSize(QWidget *widget, QString text);
float m_defaultPointSize;
};
#endif
NewLabel.cpp
#define FONT_PRECISION (0.5)
NewLabel::NewLabel(const QString &text, QWidget *parent, Qt::WindowFlags f) : QLabel(text, parent, f)
{
m_defaultPointSize = 12;
}
NewLabel::NewLabel(QWidget *parent, Qt::WindowFlags f) :
QLabel(parent, f)
{
m_defaultPointSize = 12;
}
void NewLabel::paintEvent(QPaintEvent *e)
{
QFont newFont = font();
float fontSize = getWidgetMaximumFontSize(this, this->text());
if (fontSize < m_defaultPointSize) {
newFont.setPointSizeF(fontSize);
setFont(newFont);
}
QLabel::paintEvent(e);
}
float NewLabel::getWidgetMaximumFontSize(QWidget *widget, QString text)
{
QFont font = widget->font();
const QRect widgetRect = widget->contentsRect();
const float widgetWidth = widgetRect.width();
const float widgetHeight = widgetRect.height();
QRectF newFontSizeRect;
float currentSize = font.pointSizeF();
float step = currentSize/2.0;
/* If too small, increase step */
if (step<=FONT_PRECISION){
step = FONT_PRECISION*4.0;
}
float lastTestedSize = currentSize;
float currentHeight = 0;
float currentWidth = 0;
if (text==""){
return currentSize;
}
/* Only stop when step is small enough and new size is smaller than QWidget */
while(step>FONT_PRECISION || (currentHeight > widgetHeight) || (currentWidth > widgetWidth)){
/* Keep last tested value */
lastTestedSize = currentSize;
/* Test label with its font */
font.setPointSizeF(currentSize);
/* Use font metrics to test */
QFontMetricsF fm(font);
/* Check if widget is QLabel */
QLabel *label = qobject_cast<QLabel*>(widget);
if (label) {
newFontSizeRect = fm.boundingRect(widgetRect, (label->wordWrap()?Qt::TextWordWrap:0) | label->alignment(), text);
}
else{
newFontSizeRect = fm.boundingRect(widgetRect, 0, text);
}
currentHeight = newFontSizeRect.height();
currentWidth = newFontSizeRect.width();
/* If new font size is too big, decrease it */
if ((currentHeight > widgetHeight) || (currentWidth > widgetWidth)){
//qDebug() << "-- contentsRect()" << label->contentsRect() << "rect"<< label->rect() << " newFontSizeRect" << newFontSizeRect << "Tight" << text << currentSize;
currentSize -=step;
/* if step is small enough, keep it constant, so it converge to biggest font size */
if (step>FONT_PRECISION){
step/=2.0;
}
/* Do not allow negative size */
if (currentSize<=0){
break;
}
}
/* If new font size is smaller than maximum possible size, increase it */
else{
//qDebug() << "++ contentsRect()" << label->contentsRect() << "rect"<< label->rect() << " newFontSizeRect" << newFontSizeRect << "Tight" << text << currentSize;
currentSize +=step;
}
}
return lastTestedSize;
}

Scaling items and rendering

I am making a small game in C++11 with Qt. However, I am having some issues with scaling.
The background of my map is an image. Each pixel of that image represents a tile, on which a protagonist can walk and enemies/healthpacks can be.
To set the size of a tile, I calculat the maximum amount like so (where imageRows & imageCols is amount of pixels on x- and y-axis of the background image):
QRect rec = QApplication::desktop()->screenGeometry();
int maxRows = rec.height() / imageRows;
int maxCols = rec.width() / imageCols;
if(maxRows < maxCols){
pixSize = maxRows;
} else{
pixSize = maxCols;
}
Now that I have the size of a tile, I add the background-image to the scene (in GameScene ctor, extends from QGraphicsScene):
auto background = new QGraphicsPixmapItem();
background->setPixmap(QPixmap(":/images/map.png").scaledToWidth(imageCols * pixSize));
this->addItem(background);
Then for adding enemies (they extend from a QGraphicsPixMapItem):
Enemy *enemy = new Enemy();
enemy->setPixmap(QPixmap(":/images/enemy.png").scaledToWidth(pixSize));
scene->addItem(enemy);
This all works fine, except that on large maps images get scaled once (to a height of lets say 2 pixels), and when zooming in on that item it does not get more clear, but stays a big pixel. Here is an example: the left one is on a small map where pixSize is pretty big, the second one has a pixSize of pretty small.
So how should I solve this? In general having a pixSize based on the screen resolution is not really useful, since the QGrapicsScene is resized to fit the QGraphicsView it is in, so in the end the view still determines how big the pixels show on the screen.
MyGraphicsView w;
w.setScene(gameScene);
w.fitInView(gameScene->sceneRect(), Qt::KeepAspectRatio);
I think you might want to look at the chip example from Qt (link to Qt5 but also works for Qt4).
The thing that might help you is in the chip.cpp file:
in the paint method:
const qreal lod = option->levelOfDetailFromTransform(painter->worldTransform());
where painter is simply a QPainter and option is of type QStyleOptionGraphicsItem. This quantity gives you back a measure of the current zoom level of your QGraphicsView and thus as in the example you can adjust what is being drawn at which level, e.g.
if (lod < 0.2) {
if (lod < 0.125) {
painter->fillRect(QRectF(0, 0, 110, 70), fillColor);
return;
}
QBrush b = painter->brush();
painter->setBrush(fillColor);
painter->drawRect(13, 13, 97, 57);
painter->setBrush(b);
return;
}
[...]
if (lod >= 2) {
QFont font("Times", 10);
font.setStyleStrategy(QFont::ForceOutline);
painter->setFont(font);
painter->save();
painter->scale(0.1, 0.1);
painter->drawText(170, 180, QString("Model: VSC-2000 (Very Small Chip) at %1x%2").arg(x).arg(y));
painter->drawText(170, 200, QString("Serial number: DLWR-WEER-123L-ZZ33-SDSJ"));
painter->drawText(170, 220, QString("Manufacturer: Chip Manufacturer"));
painter->restore();
}
Does this help?

DrawArc are not drawen correctly (qt/c++)

I'm pretty new to QT and I can not understand why my arc's are drawed so bad.
I have 2 problems.
First one, which I think is just normal for such drawing, is:
If I draw with a QPainterPath a straight line will be drawed on every arc, from the end of the arc to the direction of point 0,0 but not completely to 0,0 instead it is just, i think, the half way to that point...
Second one:
If I use QPainterPath or painter.drawArc the "rings" are unsemetric if i change the pen width.
I have this code which will init my Arc's.
//Edit//
Sorry forgot to provide where w and h is created.
this->getMainWidget() returns just a QWidget where my elements are drawed.
the geometry and position of the toplevel widget and one from this->getMainWidget() are the same.
QRect mainWidgetGeo = geometry();
int w = mainWidgetGeo.width();
int h = mainWidgetGeo.height();
QPen secondPen(Qt::yellow);
secondPen.setWidth(50);
circleSeconds = new Circle(this->getMainWidget());
circleSeconds->setMaxValue(60);
circleSeconds->setValue(55);
circleSeconds->setSteps(60);
circleSeconds->setMouseTracking(true);
circleSeconds->setPen(secondPen);
circleSeconds->setGeometry(QRect(0, 0, w, h));
QPen minutePen(Qt::red);
minutePen.setWidth(100);
circleMinutes = new Circle(this->getMainWidget());
circleMinutes->setMaxValue(60);
circleMinutes->setValue(50);
circleMinutes->setSteps(60);
circleMinutes->setMouseTracking(true);
circleMinutes->setPen(minutePen);
circleMinutes->setGeometry(QRect(50, 50, w-100, h-100));
QPen hourPen(Qt::green);
hourPen.setWidth(50);
circleHours = new Circle(this->getMainWidget());
circleHours->setMaxValue(12);
circleHours->setValue(45);
circleHours->setSteps(12);
circleHours->setMouseTracking(true);
circleHours->setPen(hourPen);
circleHours->setGeometry(QRect(150, 150, w-300, h-300));
This will setup 3 Arc's.
First and third one have the same pen width of 50, the second one has 100.
For completion here is the Circle class:
#include <QtGui>
#include "Circle.h"
#include <QDebug>
Circle::Circle(QWidget *parent): QWidget(parent)
{
}
void Circle::setSteps(int i)
{
this->steps = i;
}
void Circle::setValue(int i)
{
this->value = i;
repaint();
}
void Circle::setMaxValue(int i)
{
this->maxValue = i;
}
void Circle::paintEvent(QPaintEvent *)
{
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing, true);
painter.setPen(this->pen);
int stepSize = 360/this->steps;
float devideValue = ((100.0/this->maxValue)*this->value)/100.0;
int roundedSize = this->steps*devideValue;
int angel = -1.0*16.0*(stepSize*roundedSize);
qDebug() << "steps: " << steps;
qDebug() << "stepSize: " << stepSize;
qDebug() << "devideValue: " << devideValue;
qDebug() << "roundedSize: " << roundedSize;
qDebug() << "stepSize*roundedSize: " << (stepSize*roundedSize);
qDebug() << "angel: " << angel;
qDebug() << "angel: " << angel;
painter.drawArc(this->pen.width()/2, this->pen.width()/2, this->geometry().width()-(this->pen.width()), this->geometry().height()-(this->pen.width()), 0, angel);
/*QPainterPath circle_path;
circle_path.arcTo(this->pen.width()/2, this->pen.width()/2, this->geometry().width()-(this->pen.width()), this->geometry().height()-(this->pen.width()), 0, angel);
painter.drawPath(circle_path);*/
}
void Circle::setPen(QPen pen)
{
this->pen = pen;
}
Also I have noticed that, if the pen width differs from other arc's the "starting point 0" is different for each pen width...
Here are the output's to get a better understanding what goes wrong.
At this image the first problem with the line issue also present. (QPainterPath)
This is the output with painter.drawArc
//Edit//
The expected result should be something like this. Please note that the green circle spanAngle is different from the 2 images above because i did the result with photoshop and it was easier with those spanAngles :)
It should make it know clear what my problem is.
After testing with drawEllipse i recognize the same behaviour that the pen width is smaller at 45 clock as at 90 clock.
Can anybody help me to get rid of those issues? I'm also happy with different solutions to get such opened rings.
best regards,
PrDatur
There are 2 issues. The first is that starting point of arc depends on pen's width. It can be easily fixed by settings pen.setCapStyle(Qt::FlatCap); for each used pen.
The second issue is unfilled space between arcs. I can't understand why is it happening. It's somehow connected with Qt's QPen/QPainter system, but I can't find any way to fix it.
However, I found a workaround. Create appropriate QPainterPath containing borders of your figure, and then use QPainter::fillPath instead of stroking with a pen.
A side task is to use QPainterPath::moveArcTo to move and stroke a line. As far as I see, It's not supported. We'll need the following helper function that will be used with QPainterPath::lineTo method:
QPointF my_find_ellipse_coords(const QRectF &r, qreal angle) {
QPainterPath path;
path.arcMoveTo(r, angle);
return path.currentPosition();
}
In paintEvent function:
double angle = -1.0*(stepSize*roundedSize); // removed '*16' here
QPainterPath path;
QRectF outer_rect(0, 0, width(), height());
QRectF inner_rect(pen.width(), pen.width(),
width() - pen.width() * 2, height() - pen.width() * 2);
path.arcMoveTo(outer_rect, 0);
path.arcTo(outer_rect, 0, angle);
path.lineTo(my_find_ellipse_coords(inner_rect, angle));
path.arcTo(inner_rect, angle, -angle);
path.lineTo(my_find_ellipse_coords(outer_rect, 0));
path.closeSubpath();
painter.fillPath(path, QBrush(pen.color()));
There are some other minor issues in your code. For circleHours you have set the value bigger than maxValue. Also you should omit this-> when accessing class members.
In case of any issues with my code, examine complete file I was using to test it.

Why does Qt have problems handling small QGraphicsItems

When I define several QGraphicsitems with small dimensions e.g. transforming metric lines directly into QGraphicLineItems with the same length (e.g few Meters) the whole redering system becomes very slow and even is prone to crashes. On the other hand, when the items are "bigger" (length > 100) no problems arise.
To give a more concrete example: I use the following code to transform QGraphicItems into a pixmap.
QGraphicsPixmapItem* transformToPixmapItem(std::vector<QGraphicsItem>& items,int maxRes){
QRectF boundingRect;
boostForeach(QGraphicsItem* pItem,items) {
boundingRect = boundingRect.united(pItem->boundingRect());
}
QSize size(boundingRect.size().toSize());
double const scale = std::min(16.0,double(maxRes)/boundingRect.size().width());
QPixmap pixmap(size*scale);
pixmap.fill(Qt::transparent);
QPainter p(&pixmap);
//p.setCompositionMode( QPainter::CompositionMode_Source );
p.translate(-boundingRect.topLeft()*scale);
p.scale(scale,scale);
QStyleOptionGraphicsItem opt;
boostForeach(QGraphicsItem* item,items) {
item->paint(&p, &opt, 0);
}
p.end();
deleteVector(items);
QGraphicsPixmapItem* item = new QGraphicsPixmapItem(pixmap);
item->setScale(1.0/scale);
item->setOffset(boundingRect.topLeft()*scale);
qDebug() << "Pixmap done. 1/Scale " << 1.0/scale;
return item;
}
On "bigger" items this works fine on small items it crashes, the condition seems to be 1/scale. If this value used to downsize the resulting pixmap item gets too small the function completes but the rendering of the resulting item crashes.
Besides this, as already stated the rendering of objects whose physical size has been augmented e.g. by factor 100 seems to be much faster than small objects.
Is there some general guide line on recommended Graphic dimensions and where does this effect come from ?

Why does QGraphicsItem::scenePos() keep returning (0,0)

I have been toying with this piece of code:
QGraphicsLineItem * anotherLine = this->addLine(50,50, 100, 100);
qDebug() << anotherLine->scenePos();
QGraphicsLineItem * anotherLine2 = this->addLine(80,10, 300, 300);
qDebug() << anotherLine2->scenePos();
Where the this pointer refers to a QGraphicsScene. In both cases, I get QPointF(0,0) for both output.From reading the document, I thought scenePos() is supposed to return the position of the line within the scene, not where it is within its local coordinate system. What am I doing wrong?
After reading the QT 4.5 documentation carefully on addLine, I realize what I have been doing wrong. According to the doc:
Note that the item's geometry is
provided in item coordinates, and its
position is initialized to (0, 0)
So if I specify addLine(50,50, 100, 100), I am actually modifying its local item coordinate. The assumption I made that it will be treated as a scene coordinate is wrong or unfounded. What I should be doing is this
// Create a line of length 100
QGraphicsItem * anotherLine = addLine(0,0, 100, 100);
// move it to where I want it to be within the scene
anotherLine->setPos(50,50);
So if I am adding a line by drawing within the scene, I need to reset its centre to (0,0) then use setPos() to move it to where I want it to be in the scene.
Hope this helps anyone who stumble upon the same problem.