What is the most efficient way display threaded graphics in Qt? - c++

This isn't my project, but for simplicity's sake let's say I just need to print moving squares (which is what I actually did as an intermediate step). I have seen so many ways to make graphics in Qt but it's really unclear to me what is the "best" way.
I want my graphics running in a separate thread of course, and I have made this happen by simply painting to a widget or by moving the painting functionality into its own thread and also by painting to an image in a different thread and then displaying the image. I do this by making a QObject and then assigning it to a QThread, not by subclassing QThread and reimplementing run().
These ways all slow down after a couple thousand squares moving around the screen. I thought that this would be the simplest way to display graphics but I know there are other ways using openGL or QGraphicsView. I'm hoping stack overflow can save me research time and point me in the right direction.
Also, in case I am doing it the right way but implementing it wrong, here is some of my code:
In the render area:
RenderArea::RenderArea(QWidget *parent) : QWidget(parent)
{
m_image = new QImage(size(), QImage::Format_ARGB32_Premultiplied);
m_imageThread = new QThread();
m_imageMaker = new ImageMaker(size());
m_imageMaker->moveToThread(m_imageThread);
setFocusPolicy(Qt::StrongFocus);
QTimer* timer = new QTimer(this);
//Frame rate approximations: 60FPS ~ 17 ms, 30FPS ~ 33 ms
timer->start(33);
connect(timer, SIGNAL(timeout()), this, SLOT(requestImageUpdate()));
connect(this, SIGNAL(newImageParams(const QSize &)),
m_imageMaker, SLOT(makeImage(const QSize &)));
connect(m_imageMaker, SIGNAL(theImage(const QImage &)), this, SLOT(updateImage(const QImage &)));
connect(m_imageThread, SIGNAL(finished()), m_imageMaker, SLOT(deleteLater()));
connect(this, SIGNAL(doubleEntities()), m_imageMaker, SLOT(doubleEntities()));
connect(this, SIGNAL(halveEntities()), m_imageMaker, SLOT(halveEntities()));
m_imageThread->start();
}
RenderArea::~RenderArea()
{
m_imageThread->quit();
m_imageThread->deleteLater();
}
void RenderArea::paintEvent(QPaintEvent * /* event */)
{
QPainter widgetPainter(this);
widgetPainter.drawImage(0,0,*m_image);
}
void RenderArea::updateImage(const QImage& image) {
*m_image = image;
m_displayUpdatePending = false;
update();
}
void RenderArea::requestImageUpdate() {
if(!m_displayUpdatePending) {
m_displayUpdatePending = true;
emit newImageParams(size());
}
}
void RenderArea::keyPressEvent(QKeyEvent *event) {
switch (event->key()) {
case Qt::Key_Plus:
emit doubleEntities();
break;
case Qt::Key_Minus:
emit halveEntities();
break;
default:
QWidget::keyPressEvent(event);
}
}
In the image maker:
ImageMaker::ImageMaker(QSize parentSize, QObject* parent) : QObject(parent)
{
m_numEntities = 128;
m_upperBound = 0;
m_rightBound = parentSize.width();
m_lowerBound = parentSize.height();
m_leftBound = 0;
//seed the random number generator
QTime time = QTime::currentTime();
qsrand((uint)time.msec());
m_mutex.lock();
randomizeEntities();
m_mutex.unlock();
}
ImageMaker::~ImageMaker()
{
}
void ImageMaker::makeImage(const QSize & parentSize) {
m_mutex.lock();
advanceEntities();
m_rightBound = parentSize.width();
m_lowerBound = parentSize.height();
QImage image(parentSize, QImage::Format_ARGB32_Premultiplied);
QPainter imagePainter(&image);
imagePainter.setRenderHint(QPainter::Antialiasing, true);
imagePainter.setPen(QColor(0,0,0));
for (int i = 0; i < m_numEntities; ++i) {
imagePainter.drawRect(m_entityList.at(i).at(0),m_entityList.at(i).at(1),10,10);
}
imagePainter.setPen(QColor(255,0,0));
imagePainter.drawText(10,10,QString::number(m_numEntities));
imagePainter.end();
m_mutex.unlock();
emit theImage(image);
}
void ImageMaker::randomizeEntities() {
m_entityList.clear();
for(int i = 0; i < m_numEntities; ++i) {
QList<int> thisRow;
thisRow.push_back(qrand() % (m_rightBound + 1)); //random starting X coordinate
thisRow.push_back(qrand() % (m_lowerBound + 1)); //random starting Y coordinate
int tempRandX = (qrand() % 4) + 1;
int tempRandY = (qrand() % 4) + 1;
tempRandX *= (qrand() % 2) ? -1 : 1;
tempRandY *= (qrand() % 2) ? -1 : 1;
thisRow.push_back(tempRandX); //random starting X velocity
thisRow.push_back(tempRandY); //random starting Y velocity
m_entityList.push_back(thisRow);
}
}
void ImageMaker::advanceEntities() {
for (int i = 0; i < m_numEntities; ++i) {
QList<int> thisRow = m_entityList.at(i);
int xPos = thisRow.at(0);
int yPos = thisRow.at(1);
int xVel = thisRow.at(2);
int yVel = thisRow.at(3);
xPos += xVel;
yPos += yVel;
if ((xPos < 0 && xVel < 0) || (xPos > m_rightBound && xVel > 0)) xVel *= -1;
if ((yPos < 0 && yVel < 0) || (yPos > m_lowerBound && yVel > 0)) yVel *= -1;
thisRow.clear();
thisRow << xPos << yPos << xVel << yVel;
m_entityList.replace(i, thisRow);
}
}
void ImageMaker::halveEntities() {
m_mutex.lock();
if (m_numEntities > 16) {
m_numEntities /= 2;
}
randomizeEntities();
m_mutex.unlock();
}
void ImageMaker::doubleEntities() {
m_mutex.lock();
m_numEntities *= 2;
randomizeEntities();
m_mutex.unlock();
}

If you are trying to optimize your rendering speed by tackling multithreading optimizations you are doing it wrong.
What you should try to do is to batch the rendered primitives in only one draw call if possible (so, instead of drawing 1000 times 1 square, you should try drawing 1000 squares in one go).
I advise you to point your research in the OpenGl rendering optimizations direction (and learn how QT batches objects).

You could use QGraphicsScene and 1000 QGraphicsRectItem.
This is the most portable and efficient way to do this (QGraphicsScene is quite optimized).
The most efficient way may be using OpenGL, but I am not sure it would worth the effort.

Related

QScrollArea - Resize content widgets by keeping the aspect ratio

I have a layout that looks like this.
Where:
Blue: rectangle it's a ScrollArea
Orange: rectangles are the widgets from that ScrollArea
My code:
#include <QtWidgets>
///////////////////////////////////////////////////////////////////////////////////////
class RoundedPolygon : public QPolygon {
public:
RoundedPolygon() { SetRadius(10); }
void SetRadius(unsigned int iRadius) { m_iRadius = iRadius; }
const QPainterPath &GetPath() {
m_path = QPainterPath();
if (count() < 3) {
qDebug() << "!! Polygon should have at least 3 points !!";
return m_path;
}
QPointF pt1;
QPointF pt2;
for (int i = 0; i < count(); i++) {
pt1 = GetLineStart(i);
if (i == 0)
m_path.moveTo(pt1);
else
m_path.quadTo(at(i), pt1);
pt2 = GetLineEnd(i);
m_path.lineTo(pt2);
}
// close the last corner
pt1 = GetLineStart(0);
m_path.quadTo(at(0), pt1);
return m_path;
}
private:
QPointF GetLineStart(int i) const {
QPointF pt;
QPoint pt1 = at(i);
QPoint pt2 = at((i + 1) % count());
float fRat = m_iRadius / GetDistance(pt1, pt2);
if (fRat > 0.5f)
fRat = 0.5f;
pt.setX((1.0f - fRat) * pt1.x() + fRat * pt2.x());
pt.setY((1.0f - fRat) * pt1.y() + fRat * pt2.y());
return pt;
}
QPointF GetLineEnd(int i) const {
QPointF pt;
QPoint pt1 = at(i);
QPoint pt2 = at((i + 1) % count());
float fRat = m_iRadius / GetDistance(pt1, pt2);
if (fRat > 0.5f)
fRat = 0.5f;
pt.setX(fRat * pt1.x() + (1.0f - fRat) * pt2.x());
pt.setY(fRat * pt1.y() + (1.0f - fRat) * pt2.y());
return pt;
}
float GetDistance(QPoint pt1, QPoint pt2) const {
int fD = (pt1.x() - pt2.x()) * (pt1.x() - pt2.x()) + (pt1.y() - pt2.y()) * (pt1.y() - pt2.y());
return sqrtf(fD);
}
private:
QPainterPath m_path;
unsigned int m_iRadius{};
};
class PolygonButtonWidget : public QWidget {
Q_OBJECT
public:
explicit PolygonButtonWidget(QWidget *parent = nullptr) : QWidget(parent) {}
~PolygonButtonWidget() override = default;
protected:
void resizeEvent(QResizeEvent *event) override {
float ratioW = 8;
float ratioH = 3;
// ui->scrollAreaWidgetContents->setFixedSize(5000, h);
float thisAspectRatio = (float) event->size().width() / event->size().height();
if (thisAspectRatio < ratioW / ratioH) {
float w = event->size().height() * ratioW / ratioH;
float h = event->size().height();
qDebug() << hasHeightForWidth() << " " << w << " " << h;
this->resize(w, h);
if (m_nrButtons != 0) {
this->move((w + 20) * m_nrButtons, this->y());
}
}
QWidget::resizeEvent(event);
}
int m_nrButtons{};
public:
void setMNrButtons(int mNrButtons) {
m_nrButtons = mNrButtons;
}
protected:
void paintEvent(QPaintEvent *event) override {
int offset = 50;
m_polygon.clear();
m_polygon.emplace_back(0, height()); //DOWN-LEFT
m_polygon.emplace_back(width() - offset, height()); //DOWN-RIGHT
m_polygon.emplace_back(width(), 0); //TOP-RIGHT
m_polygon.emplace_back(0 + offset, 0);
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
RoundedPolygon poly;
poly.SetRadius(15);
for (QPoint point: m_polygon) {
poly << point;
}
QBrush fillBrush;
fillBrush.setColor(Qt::darkBlue);
fillBrush.setStyle(Qt::SolidPattern);
QPainterPath path;
path.addPath(poly.GetPath());
painter.fillPath(path, fillBrush);
}
void mousePressEvent(QMouseEvent *event) override {
auto cursorPos = mapFromGlobal(QCursor::pos());
qDebug() << "X: " << cursorPos.x() << " Y: " << cursorPos.y();
inside(cursorPos, m_polygon);
qDebug() << "Pressed";
}
private:
std::vector<QPoint> m_polygon;
bool inside(QPoint point, std::vector<QPoint> polygon) {
auto x = point.x();
auto y = point.y();
auto inside = false;
auto i = 0;
auto j = polygon.size() - 1;
while (i < polygon.size()) {
auto xi = polygon[i].x();
auto yi = polygon[i].y();
auto xj = polygon[j].x();
auto yj = polygon[j].y();
auto intersect = ((yi > y) != (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi);
if (intersect) inside = !inside;
j = i++;
}
qDebug() << inside;
return inside;
}
};
///////////////////////////////////////////////////////////////////////////////////////
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
QWidget root;
QHBoxLayout layout{&root};
for (int i = 0; i < 10; ++i) {
auto p = new PolygonButtonWidget();
p->setMinimumSize(100, 100);
p->setMNrButtons(i);
layout.addWidget(p);
}
root.setStyleSheet("background-color: rgb(19,19,19);");
QScrollArea view;
view.setWidget(&root);
view.show();
app.exec();
}
#include "main.moc"
The problem arises when I'm trying to resize the window. In the moment of resizing, I want my widgets to keep their aspect ratio. But that's not going to happen.
I have scroll list of widgets which is looking like this (if it's expended on X way too much)
If I will scale it on Y-axis it's going to look like this.
After I've changed the resizeEvent now it's going to look something like this
or like this
How can I fix this? For some reason, some of my widgets are going to disappear, what should be my approach in order to fix this issue?
The problem is caused by the assumption that there's any mechanism that will automatically resize the widgets for you. There isn't. A QScrollArea acts as a layout barrier and any layouts inside of it are isolated from its size, and thus from any resize events.
You must resize the container widget (the one with blue outline on your diagram) yourself anytime the scroll area changes size, and you need first to prepare a test case for the widgets such that their size changes are properly managed when placed in the layout of your choice, and said layout is resized.
Finally, the pet peeve of mine: It's unlikely that you actually need the QMainWindow for anything. It's just a silly Qt Creator template. But unless you want an MDI interface and docking, you shouldn't be using the QMainWindow - and especially not when making a self-contained example. All you need here is QScrollArea as a top-level widget. That's literally all. Any QWidget can be a top-level window!
For future submissions, please provide all the code needed in a single main.cpp file that begins with #include <QtWidgets> and ends with #include "main.moc". You won't need any other includes for Qt classes, and you can write class definitions Java-style, with all the methods defined within the class declaration itself. This provides for short code - after all, a SO question isn't an Enterprise project. It's supposed to be minimal, and that really means that anything not necessary must be removed. No need for header files, multiple includes, nor other fluff - i.e. use Qt containers instead of C++ STL so that you don't need more includes etc.
Your example should look roughly as follows:
#include <QtWidgets>
class PolygonButtonWidget : public QAbstractButton {
Q_OBJECT
/* without seeing the code here, your question is unanswerable */
};
int main(int argc, char* argv[]) {
QApplication app(argc, argv);
QWidget root;
QHBoxLayout layout{&root};
PolygonButtonWidget buttons[10];
for (auto &button : buttons)
layout.addWidget(&button);
QScrollArea view;
view.setWidget(&root);
view.show();
app.exec();
view.takeWidget();
}
#include "main.moc"
Without such an example, your question is hard to answer, since:
How can we debug it? Debugging means using a debugger. If your code cannot be immediately compiled, then it's quite unlikely that someone will bother debugging it, and debugging by inspection is often error-prone.
How can we provide a tested answer if we'd have to first write the entire "test case" for it?
How can we know what's inside your button widget? The behavior of that widget does affect the ultimate solution.
It'd also help if you described a few use cases that you'd expect to work. That is, mock up (with a drawing) the state of the widgets before and after the view is resized, so that we can easily see what it is that you expect to happen. A lot of it is very easy to miss when explaining your needs in words. Use cases are a lingua franca of software specifications. If you don't use them, it's highly likely that you yourself don't know what behavior you expect in all cases.

Timer function distorting a draggable line segment

I've a draggable line segment. Also I've got a timer. This line segment is getting distorted when I operate a timer in this window.
protected:
// override / make our own of these function to track mouse movement and
void mousePressEvent(QMouseEvent *event) ;
void mouseReleaseEvent(QMouseEvent *event) ;
void mouseMoveEvent(QMouseEvent *event) ;
I've noticed this line segment is getting distorted (see figure) when I enable a timer (basically there is a digital clock to see running time in the left side of the widget).
I've following three functions for the timer:
public slots:
void update();
void startStopTimer();
void resetTimer();
//slot
connect(ui->pushButton_stimStart, &QPushButton::clicked, this, &ProgramKeyGripV2::startStopTimer);
connect(ui->pushButton_stimStop, &QPushButton::clicked, this, &ProgramKeyGripV2::resetTimer);
QTimer *timer2 = new QTimer(this);
connect(timer2, SIGNAL(timeout()), this, SLOT(update()));
timer2->start(10);
void ProgramKeyGripV2::startStopTimer()
{
if(watch->isRunning()) {
//ui->startStopButton->setText("Restart");
watch->pause();
}
else {
//ui->startStopButton->setText("Pause");
watch->start();
}
}
void ProgramKeyGripV2::resetTimer()
{
ui->hundredthsText->setText("00");
ui->secondsText->setText("00");
ui->minutesText->setText("00");
watch->reset();
}
void ProgramKeyGripV2::update()
{
QPalette p = ui->secondsText->palette();
if(watch->isRunning())
{
qint64 time = watch->getTime();
int h = time / 1000 / 60 / 60;
int m = (time / 1000 / 60) - (h * 60);
int s = (time / 1000) - (m * 60);
int ms = time - ( s + ( m + ( h * 60)) * 60) * 1000;
int ms_dis = ms / 10;
if(ms_dis < 10) {
ui->hundredthsText->setText(QStringLiteral("0%1").arg(ms_dis));
}
else {
ui->hundredthsText->setText(QStringLiteral("%1").arg(ms_dis));
}
if(s < 10) {
ui->secondsText->setText(QStringLiteral("0%1").arg(s));
// p.setColor(QPalette::Base, Qt::white);
//ui->secondsText->setPalette(p);
}
else {
ui->secondsText->setText(QStringLiteral("%1").arg(s));
}
if(m < 10) {
ui->minutesText->setText(QStringLiteral("0%1").arg(m));
}
else {
ui->minutesText->setText(QStringLiteral("%1").arg(m));
}
}
}
This problem is not there if I disable the timer. Can you spot an issue here?
Could it be that your update method clashes with QWidget::update? (see https://doc.qt.io/qt-5/qwidget.html#update)
I'm not entire sure about what class your code is running it (what is the base class), but it seems like a plausable issue to me. Try renaming your update method to onTimeout or something like that instead.
Also, try using the &YourClass::yourSlot syntax instead of SLOT(yourSlot()), as the latter is going away.

Draw waveform from raw data using QAudioProbe

For some strange reason QAudioRecorder::audioInputs() returns twice as much devices that I actually have
They're seem to be duplicated but not really - looks like they giving different samples, because when I'm trying to play recorded audio from first two devices it sounds twice as fast, when second two devices sounds normally.
Heres my code:
#include "MicrophoneWidget.h"
#include <QLayout>
#include <sndfile.h>
MicrophoneWidget::MicrophoneWidget(QWidget *parent) : QWidget(parent)
{
QAudioEncoderSettings settings;
settings.setCodec("audio/PCM");
settings.setQuality(QMultimedia::HighQuality);
settings.setChannelCount(1);
recorder = new QAudioRecorder(this);
recorder->setEncodingSettings(settings);
button = new QPushButton();
button->setCheckable(true);
devicesBox = new QComboBox();
connect(devicesBox, SIGNAL(currentIndexChanged(int)), this, SLOT(onDeviceChanged(int)));
for(const QString& device : recorder->audioInputs()) devicesBox->addItem(device, QVariant(device));
label = new QLabel();
connect(button, SIGNAL(toggled(bool)), this, SLOT(onButtonToggled(bool)));
QVBoxLayout* layout = new QVBoxLayout();
layout->addWidget(devicesBox);
layout->addWidget(button);
layout->addWidget(label);
setLayout(layout);
probe = new QAudioProbe();
probe->setSource(recorder);
connect(probe, SIGNAL(audioBufferProbed(QAudioBuffer)), this, SLOT(onAudioBufferProbed(QAudioBuffer)));
}
void MicrophoneWidget::resizeEvent(QResizeEvent*)
{
pixmap = QPixmap(label->size());
}
void MicrophoneWidget::onAudioBufferProbed(QAudioBuffer buffer)
{
qDebug() << buffer.byteCount() / buffer.sampleCount();
const qint32 *data = buffer.constData<qint32>();
pixmap.fill(Qt::transparent);
painter.begin(&pixmap);
int count = buffer.sampleCount() / 2;
float xScale = (float)label->width() / count;
float center = (float)label->height() / 2;
for(int i = 0; i < count; i++) samples.push_back(data[i]);
for(int i = 1; i < count; i++)
{
painter.drawLine(
(i - 1) * xScale,
center + ((float)(data[i-1]) / INT_MAX * center),
i * xScale,
center + ((float)(data[i]) / INT_MAX * center)
);
}
painter.end();
label->setPixmap(pixmap);
}
void MicrophoneWidget::onButtonToggled(bool toggled)
{
if(toggled)
{
samples.clear();
recorder->record();
}
else
{
recorder->stop();
SF_INFO sndFileInfo;
sndFileInfo.channels = 1;
sndFileInfo.samplerate = 44100;
sndFileInfo.format = SF_FORMAT_WAV | SF_FORMAT_PCM_32;
QString filePath = "customWAV-" + QString::number(QDateTime::currentMSecsSinceEpoch()) + ".wav";
SNDFILE* sndFile = sf_open(filePath.toStdString().c_str(), SFM_WRITE, &sndFileInfo);
if(sndFile != nullptr)
{
sf_count_t count = sf_write_int(sndFile, samples.data(), samples.size());
qDebug() << "Written " << count << " items; " << (samples.size() / sndFileInfo.samplerate) << " seconds";
}
sf_close(sndFile);
}
}
void MicrophoneWidget::onDeviceChanged(int index)
{
recorder->stop();
recorder->setAudioInput(devicesBox->itemData(index).toString());
if(button->isChecked())recorder->record();
}
So, how should I parse the raw data to draw correct waveform?
Firs of all check that the buffer hast exactly the sample type that you expect, to do it, check the QAudioFormat sampleType function. There are 3 alternatives:
QAudioFormat::SignedInt,
QAudioFormat::UnSignedInt,
QAudioFormat::Float
This should help you to decide the correct cast for the given samples. In my case, as the different Qt examples, I use:
const qint16 *data = buffer.data<qint16>();
And them you can normalise it easily using this function:
qreal getPeakValue(const QAudioFormat& format)
{
// Note: Only the most common sample formats are supported
if (!format.isValid())
return qreal(0);
if (format.codec() != "audio/pcm")
return qreal(0);
switch (format.sampleType()) {
case QAudioFormat::Unknown:
break;
case QAudioFormat::Float:
if (format.sampleSize() != 32) // other sample formats are not supported
return qreal(0);
return qreal(1.00003);
case QAudioFormat::SignedInt:
if (format.sampleSize() == 32)
#ifdef Q_OS_WIN
return qreal(INT_MAX);
#endif
#ifdef Q_OS_UNIX
return qreal(SHRT_MAX);
#endif
if (format.sampleSize() == 16)
return qreal(SHRT_MAX);
if (format.sampleSize() == 8)
return qreal(CHAR_MAX);
break;
case QAudioFormat::UnSignedInt:
if (format.sampleSize() == 32)
return qreal(UINT_MAX);
if (format.sampleSize() == 16)
return qreal(USHRT_MAX);
if (format.sampleSize() == 8)
return qreal(UCHAR_MAX);
break;
}
return qreal(0);
}
Now you should iterate over the vector and divide by the peak that the functions returns, this will give a range of samples from [-1, 1], so save it in a QVector array to plot it.
To plot it, you have different alternatives, Qt introduce his own QtCharts module but you can still use QCustomPlot or Qwt. An example with QCustomPlot:
QCPGraph myPlot = ui->chart->addGraph();
myPlot->setData(xAxys.data(), recorded.data()); // init an X vector from 0 to the size of Y or whatever you want
ui->chart->yAxis->setRange(QCPRange(-1,1)); // set the range
ui->chart->replot();
You can find a complete example in the Qt examples or check my little GitHub project, LogoSpeech Studio, you will find complete example of how to plot the wave form, spectrogram, pitch and different properties of a signal.

Dynamically change font size of QLabel to fit available space

I'm trying to make an horizontal layout with 3 QLabel scale use all its available space. More specifically, this is what I have
this is what I am aiming for
At the moment, the second image is achieved by changing the stylesheet of the qlabels with a slider. Additionally, since I have the the three labels in a layout inside a groupbox, the groupbox resizes to fit its contents, cool.
Now I wanted to drop the slider approach and instead autofit the space available when moving the splitters. In this question, OP reimplements the resizeEvent, and I've seen other posts suggesting the same, changing point by point with this while( !doesFit ) or something similar.
I tried using this approach, both on the resize event and on the splitterMoved event. However, this approach is way prone to feedback loops and other display errors caused. In the other question, they suggest enabling ignoreSizePolicy to prevent the size policy retriggering the sizeevent, but I like how qt handles the size of the layout, how it keeps a minimum size and then it folds the widget if the user insists. Maybe it would work if the HLayout would ignore the resize events triggered by the QLabels, still IMHO unclean thought.
I was wondering if that's the recommended way of achieving this, and wether a less unstable solution exists, maybe using using stylesheets. There are some behaviours that I could also drop, the minimum size limit (so the user could potentially hide the groupbox).
If that's the recommended way of doing it, how should I use the fontmetrics if I have three separate labels, one of which (the number) changes its text dynamically and rapidly? It should not have an impact on performance, and that while loop makes me wary.
It doesn't sound like the while(!fit) approach is going to cut it. Or does it?
--- Edit regarding the duplicate question
Another post creates an event filter, which might also work if reworked to deal with a layout with 3 labels. I finally used a version of the first mentioned post with the variation of the post mentioned in the comments. I'll post the answer if the question is reopened.
One could apply the Newton's method approach from this answer to work on all widgets in a given layout. It will work on any widget with a settable font, not only on a QLabel.
The Newton's algorithm converges reasonably quickly when given a good starting point, e.g. when resizing interactively. It's not atypical to have the loop execute only once. On the other hand, QWidget::sizeHint is integer-valued and and widgets may round fractional font sizes, thus sometimes the iteration is a bit slower than one would expect. The number of iterations is capped to ensure decent performance.
A custom replacement for the label, that provided a QSizeF sizeHintF(), would work better here.
The minimum sizing for the widgets is a bit of a stretch, as the size is not updated as the widget contents change. This could be remedied easily, though.
// https://github.com/KubaO/stackoverflown/tree/master/questions/label-text-size-vert-40861305
#include <QtWidgets>
class LabelStretcher : public QObject {
Q_OBJECT
static constexpr const char kMinimumsAcquired[] = "ls_minimumsAcquired";
static constexpr const char kStretcherManaged[] = "ls_stretcherManaged";
public:
LabelStretcher(QObject *parent = 0) : QObject(parent) {
apply(qobject_cast<QWidget*>(parent));
}
void apply(QWidget *widget) {
if (!widget) return;
setManaged(widget);
setMinimumSize(widget);
widget->installEventFilter(this);
}
void setManaged(QWidget *w, bool managed = true) {
w->setProperty(kStretcherManaged, managed);
}
protected:
bool eventFilter(QObject * obj, QEvent * ev) override {
auto widget = qobject_cast<QWidget*>(obj);
if (widget && ev->type() == QEvent::Resize)
resized(widget);
return false;
}
private:
void onLayout(QLayout *layout, const std::function<void(QWidget*)> &onWidget) {
if (!layout) return;
auto N = layout->count();
for (int i = 0; i < N; ++i) {
auto item = layout->itemAt(i);
onWidget(item->widget());
onLayout(item->layout(), onWidget);
}
}
void setFont(QLayout *layout, const QFont &font) {
onLayout(layout, [&](QWidget *widget){ setFont(widget, font); });
}
void setFont(QWidget *widget, const QFont &font) {
if (!widget || !widget->property(kStretcherManaged).toBool()) return;
widget->setFont(font);
setFont(widget->layout(), font);
}
void setMinimumSize(QWidget *widget) {
if (widget->layout()) return;
widget->setMinimumSize(widget->minimumSizeHint());
}
static int dSize(const QSizeF & inner, const QSizeF & outer) {
auto dy = inner.height() - outer.height();
auto dx = inner.width() - outer.width();
return std::max(dx, dy);
}
qreal f(qreal fontSize, QWidget *widget) {
auto font = widget->font();
font.setPointSizeF(fontSize);
setFont(widget, font);
auto d = dSize(widget->sizeHint(), widget->size());
qDebug() << "f:" << fontSize << "d" << d;
return d;
}
qreal df(qreal fontSize, qreal dStep, QWidget *widget) {
fontSize = std::max(dStep + 1.0, fontSize);
return (f(fontSize + dStep, widget) - f(fontSize - dStep, widget)) / dStep;
}
void resized(QWidget *widget) {
qDebug() << "pre: " << widget->minimumSizeHint() << widget->sizeHint() << widget->size();
if (!widget->property(kMinimumsAcquired).toBool()) {
onLayout(widget->layout(), [=](QWidget *widget){ setMinimumSize(widget); });
widget->setProperty(kMinimumsAcquired, true);
}
// Newton's method
auto font = widget->font();
auto fontSize = font.pointSizeF();
qreal dStep = 1.0;
int i;
for (i = 0; i < 10; ++i) {
auto prevFontSize = fontSize;
auto d = df(fontSize, dStep, widget);
if (d == 0) {
dStep *= 2.0;
continue;
}
fontSize -= f(fontSize, widget)/d;
fontSize = std::max(dStep + 1.0, fontSize);
auto change = fabs(prevFontSize - fontSize)/fontSize;
qDebug() << "d:" << d << " delta" << change;
if (change < 0.01) break; // we're within 1% of target
}
font.setPointSizeF(fontSize);
setFont(widget, font);
qDebug() << "post:" << i << widget->minimumSizeHint() << widget->sizeHint() << widget->size();
}
};
constexpr const char LabelStretcher::kMinimumsAcquired[];
constexpr const char LabelStretcher::kStretcherManaged[];
int main(int argc, char ** argv) {
QApplication app{argc, argv};
QWidget w;
QGridLayout layout{&w};
LabelStretcher stretch{&w};
QLabel labels[6];
QString texts[6] = {"V", "30.0", "kts", "H", "400.0", "ft"};
int i = 0, j = 0, k = 0;
for (auto & label : labels) {
stretch.setManaged(&label);
label.setFrameStyle(QFrame::Box);
label.setText(texts[k++]);
if (j == 0) label.setAlignment(Qt::AlignRight | Qt::AlignVCenter);
else if (j == 1) label.setAlignment(Qt::AlignCenter);
layout.addWidget(&label, i, j++);
if (j >= 3) { i++; j=0; }
}
w.show();
return app.exec();
}
#include "main.moc"
Althought I consider KubaOber's answer better, I'll post this in case it's helpful to someone who wants a solution in the line of the answers mentioned in the post.
Note that the sampletext could be retrieved from the labels as well, the font from the stylesheet, and the code could potentially be placed on a resizeEvent of the groupbox or layout. It wouldn't work on the resizeEvent of the labels since they would compete for the space.
That is one reason why KubaOber answer is superior. Other reasons I can think of is stability given that the 3 labels space differs from the sampletext, thus the font size is not as accurate as it could be. Therefore a resize event could potentially be triggered again by the font change.
static void fitGroupBoxLabels(QGroupBox* groupbox, const QFont &samplefont, const QLayout* const samplelayout)
{
groupbox->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored);
QString sampletext = "V 1000.0 kts";
QRect availablerect = samplelayout->contentsRect();
if(samplefont.pointSizeF() <= 0) return; //not initalized yet, return
QRect textrect = QFontMetrics(samplefont).boundingRect(sampletext);
if(!textrect.isValid() || !availablerect.isValid()) return; //not initalized yet, return
float factorh = availablerect.width() / (float)textrect.width();
float factorw = availablerect.height() / (float)textrect.height();
float factor = std::min(factorh, factorw);
if (factor < 0.95 || factor > 1.05)
{
float fontSize = samplefont.pointSizeF()*factor;
QString groupBoxStyle = QString("QGroupBox{font-size:8pt} QLabel{font-size:%1pt}").arg(fontSize);
groupbox->setStyleSheet(groupBoxStyle);
}
}
After struggling with this issue, I create DynamicFontSizeLabel and DynamicFontSizePushButton widgets. Hope it helps.
https://github.com/jonaias/DynamicFontSizeWidgets/

coordinate mapping in Qt

I've been working on a physics simulation, and I've been having some trouble with coordinates.
I haven't been able to find how the scenes are mapped, and to what objects are mapped to when I don't use mapToScene();
My Code is as follows:
Ball.cpp
Ball::Ball()
{
//random start angle
angle = (qrand() % 360);
setRotation(angle);
//set the speed
speed = 5;
//random start position
int StartX = 0;
int StartY = 0;
if((qrand() % 1))
{
StartX = (qrand() % 200);
StartY = (qrand() % 200);
}
else
{
StartX = (qrand() % 100);
StartY = (qrand() % 100);
}
setPos(mapToScene(StartX,StartY));
qDebug() << "xposition:" << pos().x();
qDebug() << "yposition:" << pos().y();
qDebug() << "xposition mapped:" << mapToScene(pos()).x();
qDebug() << "yposition mapped:" << mapToScene(pos()).y();
}
mainwindow.cpp
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
scene = new QGraphicsScene (this);
ui->graphicsView->setScene (scene);
ui->graphicsView->setRenderHint(QPainter::Antialiasing);
double sceneULx = -400;
double sceneULy = 200;
double scenewidth = 800;
double sceneheight = 400;
scene->setSceneRect(sceneULx, sceneULy,scenewidth,sceneheight);
QPen mypen = QPen(Qt::red);
QLineF TopLine(scene->sceneRect().topLeft(),scene->sceneRect().topRight());
QLineF LeftLine(scene->sceneRect().topLeft(),scene->sceneRect().bottomLeft());
QLineF RightLine(scene->sceneRect().bottomRight(),scene->sceneRect().topRight());
QLineF BottomLine(scene->sceneRect().bottomLeft(),scene->sceneRect().bottomRight());
scene->addLine(TopLine,mypen);
scene->addLine(LeftLine,mypen);
scene->addLine(RightLine,mypen);
scene->addLine(BottomLine,mypen);
int ItemCount = 5;
for(int i = 0; i <ItemCount; i++)
{
Ball *item = new Ball();
scene->addItem(item);
}
timer = new QTimer(this);
connect(timer,SIGNAL(timeout()), scene,SLOT(advance()));
timer->start(10);
}
currently all the objects I generate are not in the area of the scene, which doesn't make any sense to me since I thought that the upper left corner of the scene was 0/0 and it went into the positive when you went down or to the right.
also the qDebugs yield following results:
xposition: 25.6601
yposition: 22.306
xposition mapped: 30.392
yposition mapped: 55.9751
xposition: -56.6299
yposition: 79.0004
xposition mapped: -136.607
yposition mapped: 23.7579
xposition: 31.6448
yposition: 32.3822
xposition mapped: 76.5177
yposition mapped: 38.4172
xposition: 50.017
yposition: -97.4695
xposition mapped: -59.4804
yposition mapped: -93.9612
xposition: -44.0378
yposition: 9.25605
xposition mapped: -33.8154
yposition mapped: -34.5675
I can't really find a correlation between the mapped and the unmapped numbers and it is also very strange the the mapped numbers have decimals altough I define them with an int.
I hope somebody can help me clear my confusion with the coordinate system of this programm.
I think you have the right idea about the coordinate system, but have confused yourself with an oversight in the code.
When a Ball item is created, in the constructor, it creates random coordinates and then calls this:
setPos(mapToScene(StartX,StartY));
As the documentation for mapToScene states: -
Maps the point point, which is in this item's coordinate system, to the scene's coordinate system, and returns the mapped coordinate.
However, the code is executed in the Ball's constructor and has not yet been added to the scene, so it can't successfully map the coordinates as it has no parent item and no scene with which to map to.
You can call setPos in the constructor, but without mapToScene
setPos(StartX,StartY);
With no parent, when the item is added to the scene, it will match its scene coordinates (item->pos() == item->scenePos())
Alternatively, create a Ball item, add it to the scene, and then set its position.
for(int i = 0; i <ItemCount; i++)
{
Ball *item = new Ball();
scene->addItem(item);
// note that mapToScene is irrelevant, since there is no parent item for the Ball.
item->setPos(x, y);
}
Just for an experiment you can do the following trick:
//random start position
int StartX = 100;
int StartY = 100;
setPos(mapToScene(StartX,StartY));
qDebug() << "xposition:" << pos().x();
qDebug() << "yposition:" << pos().y();
qDebug() << "xposition mapped:" << mapToScene(pos()).x();
qDebug() << "yposition mapped:" << mapToScene(pos()).y();
You will see that coordinates are still random. Basically, you are randomizing coordinates 3 times and no wonder there is no correlation.
Also try this:
double sceneULx = 0;
double sceneULy = 0;
double scenewidth = 800;
double sceneheight = 400;