DrawArc are not drawen correctly (qt/c++) - 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.

Related

QT Rotating Pie of a rectangle without shifting center

I would like to make an application which includes rotation widget inside a circle. I started this with the AnalogClock example in qt. I wouldd like to rotate a quarter pie (quarter circle) instead of gauge. My problem is I cannot locate the pie in the center of my circle.
Below picture, I barely locate the pi in the center of circle but I would like to make the pi bigger than below picture.
enter image description here
I realized that all paintings locate inside of rectangle like below.
enter image description here
When I change the drawPie function parameters rectangle's location change and rotates based on on edge point. I can make the ractange bigger but this time drawPie center changes too like the last visual.
enter image description here
You can check the code below. It is modifed from the AnalogClock example. I would like to draw a quarter pie and make it the rotate endlessly. Any help will be appreciated. If you have a better opinion, I would like hear that too.
AnalogClock::AnalogClock(QWidget *parent) :
QWidget(parent),
ui(new Ui::AnalogClock)
{
ui->setupUi(this);
QTimer *timer = new QTimer(this);
connect(timer, &QTimer::timeout, this, QOverload<>::of(&AnalogClock::update));
timer->start(1000);
setWindowTitle(tr("Analog Clock"));
resize(200, 200);
}
void AnalogClock::paintEvent(QPaintEvent *)
{
QColor minuteColor(0, 127, 127, 191);
int side = qMin(width(), height());
QTime time = QTime::currentTime();
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
painter.translate(width() / 2, height() / 2);
painter.scale(side / 200.0, side / 200.0);
painter.setPen(Qt::NoPen);
qreal radius=10;
qreal startAngle=0;
qreal span=60;
for (int i = 0; i < 12; ++i) {
painter.drawLine(88, 0, 96, 0);
painter.rotate(30.0);
}
painter.setBrush(minuteColor);
painter.save();
painter.rotate(6.0 * time.second() );
//painter.drawConvexPolygon(minuteHand, 3);
QRect rect( -radius, -radius, radius*10, radius*10);
painter.drawPie( rect, startAngle*16, span*16 );
// painter.fillRect(rect,QBrush(Qt::green));
painter.restore();
painter.setPen(minuteColor);
for (int j = 0; j < 60; ++j) {
painter.drawLine(92, 0, 96, 0);
painter.rotate(6.0);
}
}
The drawPie() function takes a rectangle that defines the hypothetical full circle in which the pie (slice) will be painted. In this example, we want that circle to be the same as the full clock circle - same size, same center.
Now the code has normalized the coordinate system for us, so that the clock center point is at (0, 0), and the radius is 100. So we need a rectangle that has its center point in (0,0) and sides that are 2*radius long. That is:
QRect rect(-100, -100, 200, 200);
Changing the rect in the code sample to that fixes the basic issue.

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

Why does QPainter::drawPoint draw a horizontal line segment?

I'm trying to draw a 3-pixel large point with QPainter. But the following code instead draws a horizontal line with width of 3 pixels.
#include <QPainter>
#include <QImage>
int main()
{
const int w=1000, h=1000;
QImage img(w, h, QImage::Format_RGBX8888);
{
QPainter p(&img);
p.fillRect(0,0,w,h,Qt::black);
p.scale(w,h);
p.setPen(QPen(Qt::red, 3./w, Qt::SolidLine, Qt::RoundCap));
p.drawPoint(QPointF(0.1,0.1));
}
img.save("test.png");
}
Here's the top left corner of the resulting image:
I am expecting to get a point which is red circle, or at least a square — but instead I get this line segment. If I comment out p.scale(w,h) and draw the point with width 3 (instead of 3./w) at position (100,100), then I get a small mostly symmetric 3-pixel in height and width point.
What's going on? Why do I get a line segment instead of point as expected? And how to fix it, without resorting to drawing an ellipse or to avoiding QPainter::scale?
I'm using Qt 5.10.0 on Linux x86 with g++ 5.5.0. The same happens on Qt 5.5.1.
It appears that QPaintEngineEx::drawPoints renders points as line segments of length 1/63.. See the following code from qtbase/src/gui/painting/qpaintengineex.cpp in the Qt sources:
void QPaintEngineEx::drawPoints(const QPointF *points, int pointCount)
{
QPen pen = state()->pen;
if (pen.capStyle() == Qt::FlatCap)
pen.setCapStyle(Qt::SquareCap);
if (pen.brush().isOpaque()) {
while (pointCount > 0) {
int count = qMin(pointCount, 16);
qreal pts[64];
int oset = -1;
for (int i=0; i<count; ++i) {
pts[++oset] = points[i].x();
pts[++oset] = points[i].y();
pts[++oset] = points[i].x() + 1/63.;
pts[++oset] = points[i].y();
}
QVectorPath path(pts, count * 2, qpaintengineex_line_types_16, QVectorPath::LinesHint);
stroke(path, pen);
pointCount -= 16;
points += 16;
}
} else {
for (int i=0; i<pointCount; ++i) {
qreal pts[] = { points[i].x(), points[i].y(), points[i].x() + qreal(1/63.), points[i].y() };
QVectorPath path(pts, 2, 0);
stroke(path, pen);
}
}
}
Notice the pts[++oset] = points[i].x() + 1/63.; line in the opaque brush branch. This is the second vertex of the path — shifted with respect to the desired position of the point.
This explains why the line extends to the right of the position requested and why it depends on the scale. So, it seems the code in the OP isn't wrong for an ideal QPainter implementation, but just has come across a Qt bug (be it in the implementation of the method or in its documentation).
So the conclusion: one has to work around this problem by either using different scale, or drawing ellipses, or drawing line segments with much smaller lengths than what QPainter::drawPoints does.
I've reported this as QTBUG-70409.
Though I have not been able to pin point why exactly the problem occurs, I have got relatively close to the solution. The problem lies with scaling. I did a lots of trial and error with different scaling and width of point with the below code.
const int w=500, h=500;
const int scale = 100;
float xPos = 250;
float yPos = 250;
float widthF = 5;
QImage img(w, h, QImage::Format_RGBX8888);
{
QPainter p(&img);
p.setRenderHints(QPainter::Antialiasing);
p.fillRect(0,0,w,h,Qt::black);
p.scale(scale, scale);
p.setPen(QPen(Qt::red, widthF/(scale), Qt::SolidLine, Qt::RoundCap));
p.drawPoint(QPointF(xPos/scale, yPos/scale));
}
img.save("test.png");
The above code produces the image
My observations are
1) Due to high scaling, the point (which is just 3 pixel wide) is not able to scale proportionally at lower width (width of point), if you set width as something like 30 the round shape is visible.
2) If you want to keep the width of point low then you have to decrease the scaling.
Sadly I can not explain why at high scaling it is not expanding proportionally.

Qt - Adding items to QGraphicsScene with absolute position

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.

QPainter doesn't change color

I'm learning Qt. I'm failing to realize the exercise of chapter 11 of Qt tutorial, which states "Change the color of the cannon when a shot is in the air." I chose to implement the change in paintCannon function (below). What's wrong with my code below?
void CannonField::paintCannon(QPainter &painter)
{
painter.setPen(Qt::NoPen);
if (autoShootTimer->isActive()){
std::cout << "in paintCannon yellow; " << std::endl;
// This gets called everytime `paintEvent` occurs.
// Please see the code in the web page (http://doc.trolltech.com/4.3/tutorial-t11-cannonfield-cpp.html) for this part.
painter.setBrush(Qt::yellow);
}else{
std::cout << "in paintCannon blue; " << std::endl;
painter.setBrush(Qt::blue);
}
painter.save();
painter.translate(0, height());
painter.drawPie(QRect(-35, -35, 70, 70), 0, 90 * 16);
painter.rotate(-currentAngle);
painter.drawRect(barrelRect);
painter.restore();
}
Since I first suspected Qpainter's save and restore might have been doing something wrong, I commented them out which ended up re-painting nothing.
Thanks.
The problem you are having is in this routine:
void CannonField::moveShot()
{
QRegion region = shotRect();
++timerCount;
QRect shotR = shotRect();
if (shotR.x() > width() || shotR.y() > height())
{
autoShootTimer->stop();
}
else
{
region = region.unite(shotR);
}
update(region);
}
When the shot is moved, update() is being called with a region specified. This results in only the shot rectangle being repainted. If you remove the region from the call to update(), the entire widget is repainted and your color change will work correctly.