'Looking for the correct Qt5 mechanism to create an identical sized QGridLayout using a custom widget in the grid
So,I'm trying to build a small app that watches my MQTT server for messages and presents a widget with the MQTT JSON content for every message it sees. The logic works well, but I'm having a lot of trouble with adding a QScrollView to contain the grid of widgets. No matter what I do, the eventual widget size is never completely correct.
jsonwidget.h
#pragma once
#include <QtCore/QtCore>
#include <QtWidgets/QtWidgets>
class JsonWidget : public QWidget
{
Q_OBJECT
public:
explicit JsonWidget(QString topic, QWidget *parent = nullptr);
~JsonWidget() override;
void addJson(QJsonDocument &json);
QString topic() { return m_topicString; }
QSize minimumSizeHint() const override;
QSize sizeHint() const override;
protected slots:
void showEvent(QShowEvent *e) override;
void paintEvent(QPaintEvent *e) override;
private:
void populateNewWidget(int localX, QJsonObject obj);
void updateWidget(int localX, int i, QJsonObject obj);
QLabel *m_topic;
QString m_topicString;
uint32_t m_y;
uint32_t m_width;
uint32_t m_origin;
bool m_populated;
};
jsonwidget.cpp
#include "jsonwidget.h"
JsonWidget::JsonWidget(QString topic, QWidget *parent) : QWidget(parent), m_topicString(topic), m_y(0), m_width(0), m_populated(false)
{
m_topic = new QLabel(topic, this);
m_topic->setStyleSheet("QLabel { color: blue; font: 16pt 'Roboto'; }");
m_topic->move(5, m_y + 10);
m_origin = m_topic->height();
m_topic->show();
m_y = m_topic->height() + 10;
QPalette pal = palette();
pal.setColor(QPalette::Window, Qt::white);
setAutoFillBackground(true);
setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed));
setPalette(pal);
}
JsonWidget::~JsonWidget() = default;
QSize JsonWidget::minimumSizeHint() const
{
if (parentWidget())
return QSize(parentWidget()->width() / 2, m_y);
else
return QSize(20, 20);
}
QSize JsonWidget::sizeHint() const
{
if (parentWidget())
return QSize(parentWidget()->width() / 2, m_y);
else
return QSize(20, 20);
}
void JsonWidget::paintEvent(QPaintEvent* e)
{
QPainter painter(this);
painter.drawRoundedRect(0, 0, width(), height(), 0, 0);
QWidget::paintEvent(e);
}
void JsonWidget::showEvent(QShowEvent *e)
{
QWidget::showEvent(e);
m_topic->adjustSize();
}
void JsonWidget::populateNewWidget(int localX, QJsonObject obj)
{
QFont f("Roboto", 14);
localX += 10;
foreach(const QString& key, obj.keys()) {
QJsonValue value = obj.value(key);
QLabel *label;
switch (value.type()) {
case QJsonValue::Bool:
label = new QLabel(QString("%1 : %2").arg(key).arg(value.toBool()), this);
label->move(localX, m_y);
label->setObjectName(key);
m_y += label->height();
break;
case QJsonValue::Double:
label = new QLabel(QString("%1 : %2").arg(key).arg(value.toDouble()), this);
label->move(localX, m_y);
label->setObjectName(key);
m_y += label->height();
break;
case QJsonValue::String:
label = new QLabel(QString("%1 : %2").arg(key).arg(value.toString()), this);
label->move(localX, m_y);
label->setObjectName(key);
m_y += label->height();
break;
case QJsonValue::Array:
label = new QLabel(QString("%1 : %2").arg(key).arg("NOT DONE YET"), this);
label->move(localX, m_y);
m_y += label->height();
label->setObjectName(key);
break;
case QJsonValue::Object:
label = new QLabel(QString("%1").arg(key), this);
label->move(localX, m_y);
m_y += label->height();
label->setObjectName("object");
populateNewWidget(localX, value.toObject());
break;
case QJsonValue::Undefined:
case QJsonValue::Null:
label = new QLabel(QString("%1 : %2").arg(key).arg("UNDEFINED"), this);
label->move(localX, m_y);
m_y += label->height();
label->setObjectName(key);
break;
}
label->setFont(f);
uint32_t newWidth = label->width() + localX;
if (newWidth > m_width)
m_width = newWidth;
}
}
void JsonWidget::updateWidget(int localX, int i, QJsonObject obj)
{
QList<QLabel*> labels = findChildren<QLabel*>();
bool moveon = false;
int index = i + 1;
localX += 10;
foreach(const QString &key, obj.keys()) {
QJsonValue value = obj.value(key);
for (auto label : labels) {
moveon = false;
switch (value.type()) {
case QJsonValue::Bool:
if (label->objectName() == key) {
label->setText(QString("%1 : %2").arg(key).arg(value.toBool()));
moveon = true;
}
break;
case QJsonValue::Double:
if (label->objectName() == key) {
label->setText(QString("%1 : %2").arg(key).arg(value.toDouble()));
moveon = true;
}
break;
case QJsonValue::String:
if (label->objectName() == key) {
label->setText(QString("%1 : %2").arg(key).arg(value.toString()));
moveon = true;
}
break;
case QJsonValue::Array:
if (label->objectName() == key) {
label->setText("ARRAY UNFINISHED");
moveon = true;
}
break;
case QJsonValue::Object:
updateWidget(localX, index, value.toObject());
moveon = true;
break;
case QJsonValue::Undefined:
case QJsonValue::Null:
if (label->objectName() == key) {
label->setText("UNDEFINED");
moveon = true;
}
break;
}
if (moveon)
break;
}
}
}
void JsonWidget::addJson(QJsonDocument& doc)
{
if (doc.isEmpty() || doc.isNull()) {
qWarning() << __PRETTY_FUNCTION__ << ": Bad JSON document passed in";
return;
}
// qDebug() << __PRETTY_FUNCTION__ << "Topic label height" << m_topic->height();
QJsonObject json = doc.object();
if (m_populated) {
updateWidget(5, 0, json);
}
else {
populateNewWidget(5, json);
m_populated = true;
}
QDateTime now = QDateTime::currentDateTime();
m_topic->setText(QString("Topic: %1 [%2]").arg(m_topicString).arg(now.toString("dd-MM-yyyy h:mm:ss ap")));
}
Note, some of the sizing is just the result of trying things to see what works and what doesn't. The else 20,20 is so I know when parentWidget() isn't valid.
tabwidget.h
#include <QtCore/QtCore>
#include <QtWidgets/QtWidgets>
#include "jsonwidget.h"
/**
* @todo write docs
*/
class TabWidget : public QWidget
{
Q_OBJECT
public:
TabWidget(QWidget *parent = nullptr);
~TabWidget() override;
bool addJson(QString topic, QJsonDocument doc);
private:
bool addNewWidget(int row, int col, QString topic, QJsonDocument doc);
QGridLayout *m_layout;
};
tabwidget.cpp
#include "tabwidget.h"
TabWidget::TabWidget(QWidget *parent) : QWidget(parent)
{
m_layout = new QGridLayout();
setLayout(m_layout);
QPalette pal = palette();
pal.setColor(QPalette::Window, Qt::white);
setAutoFillBackground(true);
setPalette(pal);
}
TabWidget::~TabWidget() = default;
bool TabWidget::addNewWidget(int row, int col, QString topic, QJsonDocument doc)
{
JsonWidget *widget = new JsonWidget(topic);
widget->addJson(doc);
widget->setFixedWidth(parentWidget()->width() / 2);
m_layout->addWidget(widget, row, col);
m_layout->setSizeConstraint(QLayout::SetMinimumSize);
return true;
}
bool TabWidget::addJson(QString topic, QJsonDocument doc)
{
for (int i = 0; i <= m_layout->rowCount(); i++) {
for (int j = 0; j < 2; j++) {
QLayoutItem *item = m_layout->itemAtPosition(i, j);
if (item == nullptr) {
// qDebug() << __PRETTY_FUNCTION__ << "No widget at row" << i << ", column" << j << ", creating a new topic [" << topic << "]";
return addNewWidget(i, j, topic, doc);
}
JsonWidget *jw = static_cast<JsonWidget*>(item->widget());
if (jw->topic() == topic) {
// qDebug() << __PRETTY_FUNCTION__ << "Updating existing [" << topic << "] at row" << i << ", column" << j;
jw->addJson(doc);
return false;
}
}
}
// qDebug() << __PRETTY_FUNCTION__ << "Did not find [" << topic << "] in the widget set";
return false;
}
Finally, because I'll cute the QMainWindow down a bit, I'll put the function I use to create the tab
void MQTTSnoopWindow::newTab(QString topic, QJsonDocument json)
{
QMutexLocker locker(&m_newTabMutex);
QString parentTopic = topic.left(topic.indexOf("/"));
QScrollArea *sa = new QScrollArea(m_mainWidget);
sa->setBackgroundRole(QPalette::Light);
TabWidget *tab = new TabWidget(sa);
tab->addJson(topic, json);
sa->setWidget(tab);
m_mainWidget->addTab(sa, parentTopic);
m_topics++;
// qDebug() << __PRETTY_FUNCTION__ << "Created a new tab";
}
So, my question is, what's the correct way to do this kind of embedding? The goal is two columns of equal sized widgets in a grid. I know I'm doing the sizing wrong, but I'm not sure how. Also, if I had to guess, my parent/child on the objects isn't correct either. So I'm roughly guessing and reading all the layout and widget docs plus google searches for similar questions. This all works great if I don't use the scrollview too. But then I get an ever expanding window which is not what I want.
Solution 1:[1]
OK, finally figured it out. Turns out, you have to use layouts across the board, and it works. I was not aware of this. So, with some indirection, my TabWidget and JsonWidget both work correctly, and I needed to embed those in the QScrollArea which was embedded in a layout which is embedded in a widget which is set on the tab.
So, I created a single entity QHBoxLayout, and stick that in an empty parent QWidget as the layout. Then create the QScrollArea, and add that as the only widget in the layout. Finally, create my custom TabWidget and add that as the only element to the QScrollArea.
QWidget *parentWidget = new QWidget(m_mainWidget);
QHBoxLayout *parentLayout = new QHBoxLayout();
parentWidget->setLayout(parentLayout);
QScrollArea *parentScroll = new QScrollArea();
parentLayout->addWidget(parentScroll);
TabWidget *tab = new TabWidget();
parentScroll->setWidgetResizable(true);
parentScroll->setWidget(tab);
tab->addJson(topic, json);
m_mainWidget->addTab(parentWidget, parentTopic);
Then, to get my custom widget back out, you have to unwind the boxes.
QWidget *top = static_cast<QWidget*>(m_mainWidget->widget(i));
QHBoxLayout *topLayout = static_cast<QHBoxLayout*>(top->layout());
QScrollArea *scroller = static_cast<QScrollArea*>(topLayout->itemAt(0)->widget());
TabWidget *widget = static_cast<TabWidget*>(scroller->widget());
I'm going to rename a few entities here now that the layering makes the old names somewhat confusing. I left them to reference the original post. Hoping this helps someone.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Peter Buelow |