Automatically add/remove widgets in QGridLayout with fixed column width? - c++

Say I'm creating an image gallery view that has 3 columns (i.e. it displays 3 images on each row). I can add new images to the end by using division and remainder:
int row = images.size() / 3;
int col = images.size() % 3;
gridLayout->addWidget(myImage, row, col);
The problem arises when I want to remove an image from the middle; it now leaves that middle row with just two images instead of "shifting" everything back and filling each row with 3 images (except the last row).
It seems like QGridLayout doesn't have too many features, am I missing something here or is the only option to implement everything myself? Or am I using the wrong tool (QGridLayout) in the first place?

AFAIK, there is no auto-relayout in QGridLayout. I'm even not sure whether QGridLayout is intended for this purpose.
IMHO, QTableView or QTableWidget might be the better choice.
(Concerning this, QAbstractItemModel::moveRows() comes into my mind.)
However, this doesn't mean that it cannot be achieved.
I made an MCVE to demonstrate this – testQDeleteFromLayoutShift.cc:
#include <cassert>
#include <vector>
#include <QtWidgets>
// comment out to get rid of console diagnostic output
#define DIAGNOSTICS
class PushButton: public QPushButton {
public:
PushButton(const QString &text, QWidget *pQParent = nullptr):
QPushButton(text)
{ }
#ifdef DIAGNOSTICS
virtual ~PushButton() { qDebug() << "Destroyed:" << this << text(); }
#else // (not) DIAGNOSTICS
virtual ~PushButton() = default;
#endif // DIAGNOSTICS
};
// number of columns in grid
const int wGrid = 3;
// fill grid with a certain amount of buttons
std::vector<QPushButton*> fillGrid(QGridLayout &qGrid)
{
const int hGrid = 5;
std::vector<QPushButton*> pQBtns; pQBtns.reserve(wGrid * hGrid);
unsigned id = 0;
for (int row = 0; row < hGrid; ++row) {
for (int col = 0; col < wGrid; ++col) {
QPushButton *pQBtn = new PushButton(QString("Widget %1").arg(++id));
qGrid.addWidget(pQBtn, row, col);
pQBtns.push_back(pQBtn);
}
}
#ifdef DIAGNOSTICS
qDebug() << "qGrid.parent().children().count():"
<< dynamic_cast<QWidget*>(qGrid.parent())->children().count();
qDebug() << "qGrid.count():"
<< qGrid.count();
#endif // DIAGNOSTICS
return pQBtns;
}
// delete a button from grid (shifting the following)
void deleteFromGrid(QGridLayout &qGrid, QPushButton *pQBtn)
{
qDebug() << "Delete button" << pQBtn->text();
// find index of widget in grid
int i = 0;
const int n = qGrid.count();
while (i < n && qGrid.itemAt(i)->widget() != pQBtn) ++i;
assert(i < n);
// find item position in grid
int row = -1, col = -1, rowSpan = 0, colSpan = 0;
qGrid.getItemPosition(i, &row, &col, &rowSpan, &colSpan);
// remove button from layout
QLayoutItem *pQItemBtn = qGrid.itemAt(i);
qGrid.removeItem(pQItemBtn);
// reposition all following button layouts
for (int j = i + 1; j < n; ++j) {
QLayoutItem *pQItem = qGrid.takeAt(i);
const int row = (j - 1) / wGrid, col = (j - 1) % wGrid;
qGrid.addItem(pQItem, row, col);
}
delete pQBtn;
#if 1 // diagnostics
qDebug() << "qGrid.parent().children().count():"
<< dynamic_cast<QWidget*>(qGrid.parent())->children().count();
qDebug() << "qGrid.count():"
<< qGrid.count();
#endif // 0
}
// application
int main(int argc, char **argv)
{
qDebug() << "Qt Version:" << QT_VERSION_STR;
QApplication app(argc, argv);
// setup GUI
QWidget qWin;
qWin.setWindowTitle(QString::fromUtf8("Demo Delete from QGridLayout (with shift)"));
QGridLayout qGrid;
qWin.setLayout(&qGrid);
std::vector<QPushButton*> pQBtns = fillGrid(qGrid);
qWin.show();
// install signal handlers
for (QPushButton *const pQBtn : pQBtns) {
QObject::connect(pQBtn, &QPushButton::clicked,
[&qGrid, pQBtn](bool) {
QTimer::singleShot(0, [&qGrid, pQBtn]() {
deleteFromGrid(qGrid, pQBtn);
});
});
}
// runtime loop
return app.exec();
}
A Qt project file to build this – testQDeleteFromLayoutShift.pro:
SOURCES = testQDeleteFromLayoutShift.cc
QT += widgets
Output in Windows 10 (built with VS2017):
Qt Version: 5.13.0
qGrid.parent().children().count(): 16
qGrid.count(): 15
After clicking on button “Widget 8”:
Delete button "Widget 8"
Destroyed: QPushButton(0x25521e3ef10) "Widget 8"
qGrid.parent().children().count(): 15
qGrid.count(): 14
After clicking on button “Widget 6”
Delete button "Widget 6"
Destroyed: QPushButton(0x25521e3f7d0) "Widget 6"
qGrid.parent().children().count(): 14
qGrid.count(): 13
After clicking on button “Widget 12”
Delete button "Widget 12"
Destroyed: QPushButton(0x25521e46ad0) "Widget 12"
qGrid.parent().children().count(): 13
qGrid.count(): 12
Notes:
I used lambdas for the signal handlers of QPushButton::clicked() to pass the related QGridLayout and QPushButton* to the handler deleteFromGrid() – IMHO the most convenient way.
deleteFromGrid() is called via a QTimer::singleShot() with delay 0. If I would call deleteFromGrid() in the signal handler of QPushButton::clicked() directly, this would result in some kind of Harakiri due to the last line in deleteFromGrid(): delete pQBtn;.
The nested lambdas might look a bit scaring, sorry.
While writing this answer I recalled an older of mine:
SO: qgridlayout add and remove sub layouts
which might be of interest.

Related

How to get the QLineEdit text offset when have QAction at leading position

I have a QLineEdit with a QAction at leading position. I want to know the start position of the text but I don't find how to do:
QLineEdit *le = new QLineEdit(parent);
le->addAction(QIcon(":/myicon"), QLineEdit::LeadingPosition);
// Now I want to get the text start position
// but both return "QMargins(0, 0, 0, 0) QMargins(0, 0, 0, 0)"
qDebug() << le->textMargins() << le->contentsMargins();
I searched in the qt's github sources to find if the addAction() method does something on the contents or text margins but without success.
I must admit that (before reading OPs question) I was not aware about QLineEdit::addAction(). Thus, I wrote a little sample testQLineEditAction.cc:
#include <QtWidgets>
int main(int argc, char **argv)
{
qDebug() << "Qt Version:" << QT_VERSION_STR;
QApplication app(argc, argv);
// init GUI
QLineEdit qEdit;
qEdit.addAction(QIcon("./document-properties.svg"), QLineEdit::LeadingPosition);
qEdit.addAction(QIcon("./document-save.svg"), QLineEdit::TrailingPosition);
qEdit.show();
// runtime loop
return app.exec();
}
and this is how it looks (compiled in cygwin64):
Afterwards, I digged a bit through woboq.org to find out how it is implemented.
I started in QLineEdit::paintEvent():
void QLineEdit::paintEvent(QPaintEvent *)
{
...
QStyleOptionFrame panel;
initStyleOption(&panel);
...
QRect r = style()->subElementRect(QStyle::SE_LineEditContents, &panel, this);
r.setX(r.x() + d->effectiveLeftTextMargin());
r.setY(r.y() + d->topTextMargin);
r.setRight(r.right() - d->effectiveRightTextMargin());
r.setBottom(r.bottom() - d->bottomTextMargin);
This is interesting: The rectangle for contents is retrieved and then corrected by inner offsets.
QFontMetrics fm = fontMetrics();
...
QRect lineRect(r.x() + d->horizontalMargin, d->vscroll, r.width() - 2*d->horizontalMargin, fm.height());
About the d->horizontalMargin, I'm not quite sure but I ignored it for now and followed instead d->effectiveLeftTextMargin():
int QLineEditPrivate::effectiveLeftTextMargin() const
{
return effectiveTextMargin(leftTextMargin, leftSideWidgetList(), sideWidgetParameters());
}
...
static int effectiveTextMargin(int defaultMargin, const QLineEditPrivate::SideWidgetEntryList &widgets,
const QLineEditPrivate::SideWidgetParameters &parameters)
{
if (widgets.empty())
return defaultMargin;
return defaultMargin + (parameters.margin + parameters.widgetWidth) *
int(std::count_if(widgets.begin(), widgets.end(),
[](const QLineEditPrivate::SideWidgetEntry &e) {
return e.widget->isVisibleTo(e.widget->parentWidget()); }));
}
So, I came to the conclusion that QLineEditPrivate::effectiveLeftTextMargin() considers the space for action icons when effective size of text rectangle is determined.
It's a pity that all these functions are private and thus not accessable from outside. After thinking a while how to get access to these from outside and looking into doc. whether I haven't overseen something, I got the idea to use the QActions directly for this:
#include <QtWidgets>
void inspect(const QString &cmd, QAction &qCmd)
{
qDebug() << (cmd + "->associatedWidgets().size():")
<< qCmd.associatedWidgets().size();
int i = 0;
for (QWidget *const pQWidget : qCmd.associatedWidgets()) {
qDebug() << '[' << i++ << "]:"
<< typeid(*pQWidget).name()
<< "geometry:" << pQWidget->geometry();
}
}
int main(int argc, char **argv)
{
qDebug() << "Qt Version:" << QT_VERSION_STR;
QApplication app(argc, argv);
// init GUI
QLineEdit qEdit;
qEdit.setText("012345678901234567890123456789");
QAction *const pQCmd1
= qEdit.addAction(QIcon("./document-properties.svg"), QLineEdit::LeadingPosition);
QAction *const pQCmd2
= qEdit.addAction(QIcon("./document-save.svg"), QLineEdit::TrailingPosition);
qEdit.show();
qDebug() << "qEdit.geometry():" << qEdit.geometry();
inspect("pQCmd1", *pQCmd1);
inspect("pQCmd2", *pQCmd2);
// runtime loop
return app.exec();
}
Console output:
Qt Version: 5.9.4
qEdit.geometry(): QRect(0,0 200x23)
"pQCmd1->associatedWidgets().size():" 2
[ 0 ]: 9QLineEdit geometry: QRect(0,0 200x23)
[ 1 ]: 19QLineEditIconButton geometry: QRect(4,2 22x18)
"pQCmd2->associatedWidgets().size():" 2
[ 0 ]: 9QLineEdit geometry: QRect(0,0 200x23)
[ 1 ]: 19QLineEditIconButton geometry: QRect(174,2 22x18)
To compare the values, another snapshot with modified icons (frame drawn in SVGs to show icon size) which has been magnified (factor 5):
Left QLineEditIconButton reported position (4, 2) but the left frame of icon is 8 pixels away from left border of QLineEdit. There is surely a frame around the QLineEditIconButton which has to be considered as well (and I didn't investigate how to retrieve it). The width of frame might be subject of style engine and thus vary between platforms. To make such attempt robust and portable, the respective values should be retrieved from the widgets or from style. This starts to become a tedious fiddling with more or less chance for success.
I ended up once in a similar situation when trying to answer SO: How to automatically increase/decrease text size in label in Qt.
Concerning QLineEdit::cursorRect():
I believe that using QLineEdit::cursorRect() is (as well) at best fragile.
I modified my above example to check this out:
#include <QtWidgets>
class LineEdit: public QLineEdit {
public:
QRect cursorRect() const { return QLineEdit::cursorRect(); }
};
int main(int argc, char **argv)
{
qDebug() << "Qt Version:" << QT_VERSION_STR;
QApplication app(argc, argv);
// init GUI
LineEdit qEdit;
qEdit.setText("012345678901234567890123456789");
qEdit.addAction(QIcon("./document-properties.svg"), QLineEdit::LeadingPosition);
qEdit.addAction(QIcon("./document-save.svg"), QLineEdit::TrailingPosition);
qEdit.show();
qDebug() << "qEdit.cursorRect():" << qEdit.cursorRect();
// runtime loop
return app.exec();
}
Console output:
Qt Version: 5.9.4
qEdit.geometry(): QRect(0,0 200x23)
qEdit.cursorRect(): QRect(253,0 9x16)
Funny, that the cursor x-position is not only quite high – it's even higher than the width of qEdit. How comes? The initial text "012345678901234567890123456789" I put into qEdit causes the cursor to be close to the right whereby horizontal scrolling happens. The cursor position seems to be related to the virtual text width (including the clipped range on the left side).
After having to deal with this problem myself recently, I found a very simple solution:
QLineEdit.setTextMargins(24, 0, 0, 0)
Where the parameters refer to left, top, right, and bottom text margins.
I am using PyQt5, but the idea is the same for Qt as well.

C++ Qt setting up signal and slots for QMenu

I am trying to make a make a program that allows a user to right click a cell in a gridView and choose from a menu of actions.
I have the menu display, but am having trouble getting the signal and slot to work without getting errors. I am working in Qt 5.5 C++11, not very advanced with Qt or C++ yet so any help would be greatly appreciated
using namespace std;
QStandardItemModel* model;
QStandardItem *value;
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
model = new QStandardItemModel(9,9,this);
ui->tableView->setModel(model);
ui->tableView->setShowGrid(true);
ui->tableView->setWordWrap(true);
//to allow menu
ui->tableView->setContextMenuPolicy(Qt::CustomContextMenu);
for(int row = 0; row < 9; row++){
ui->tableView->setRowHeight(row, 75);
for(int col = 0; col < 9; col++){
ui->tableView->setColumnWidth(col, 75);
QFont f("Consolas");
f.setPointSize(10);
value = new QStandardItem(QString("1 2 3\n4 5 6\n7 8 9"));
if ((row < 3 && col < 3) || (row > 5 && col < 3)
|| (row < 3 && col > 5) || (row > 5 && col > 5)
|| ((row > 2 && row < 6) && (col > 2 && col < 6))){
QBrush b(QColor("Moccasin"));
value->setBackground(b);
}
value->setFont(f);
value->setTextAlignment(Qt::AlignCenter);
model->setItem(row,col,value);
}
}
//connects model so functions run when a cell's text is changed
connect(model, SIGNAL(itemChanged(QStandardItem*)), this, SLOT(on_cell_changed(QStandardItem*)));
//this sets up the menu
connect(ui->tableView, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(menuRequest(QPoint)));
}
this is the main window part of the program. It is (going to be) a sudoku solver, so the model represents the 9x9 grid and the for loop is populating the grid with choices for input (but that's not the thing I'm having trouble with). I need help with the menu part below (the above was just for context):
void MainWindow::menuRequest(QPoint pos)
{
QModelIndex index = ui->tableView->indexAt(pos);
std::cout << "MainWindow::menuRequest - at" << " QModelIndex row = " <<
index.row() << ", column = " << index.column() << std::endl;
QMenu menu(this);
QMenu setValues("Initialize Grid", this);
QAction *setValue1;
QAction *setValue2;
QAction *setValue3;
QAction *setValue4;
QAction *setValue5;
QAction *setValue6;
QAction *setValue7;
QAction *setValue8;
QAction *setValue9;
setValue1 = setValues.addAction("Set value to 1");
setValue2 = setValues.addAction("Set value to 2");
setValue3 = setValues.addAction("Set value to 3");
setValue4 = setValues.addAction("Set value to 4");
setValue5 = setValues.addAction("Set value to 5");
setValue6 = setValues.addAction("Set value to 6");
setValue7 = setValues.addAction("Set value to 7");
setValue8 = setValues.addAction("Set value to 8");
setValue9 = setValues.addAction("Set value to 9");
connect(menu, SIGNAL(triggered(QAction*)), this, SLOT(on_menu_clicked(QAction*)));
menu.addMenu(&setValues);
QAction *action = menu.exec(
ui->tableView->viewport()->mapToGlobal(pos));
}
void MainWindow::on_menu_clicked(QAction*){
cout << "test menu click worked";
}
The connect statement in the MenuRequest function is throwing an error:
connect(menu, SIGNAL(triggered(QAction*)), this, SLOT(on_menu_clicked(QAction*)));
C2664: 'QMetaObject::Connection QObject::connect(const QObject *, const char *, const char *, Qt::ConnectionType)const' : cannot convert argument 1 from 'QMenu' to 'const QObject *'
When it is supposed to go to the on_menu_click function it throws the above error
I know that means that menu isn't of the QObject type but I'm not sure how to fix this. Any help would be greatly appreciated
As stated in the comment your first argument in the connect() function needs to be a pointer. The documentation States the following:
connect(const QObject * sender, const char * signal,
const QObject * receiver, const char * method,
Qt::ConnectionType type = Qt::AutoConnection)
http://doc.qt.io/qt-5/qobject.html#connect

An issue when creating a pop-up menu w.r.t to a parametr via loop

I am trying to create pop-up menu depending on a variable as follows:
QMenu menu(widget);
for(int i = 1; i <= kmean.getK(); i++)
{
stringstream ss;
ss << i;
string str = ss.str();
string i_str = "Merge with " + str;
QString i_Qstr = QString::fromStdString(i_str);
menu.addAction(i_Qstr, this, SLOT(mergeWith1()));
}
menu.exec(position);
where:
kmean.get(K) returns an int value,
mergeWith1() is some `SLOT()` which works fine
Issue:
The loop creates an action on menu only for i=1 case, and ignores other values of i.
Additional information
When doing the same loop with casual int values (without convert) everything works fine. e.g. if I do in loop only menu.addAction(i, this, SLOT(...))) and my K=4, a menu will be created with four actions in it, named 1, 2, 3, 4 correspondingly.
What can be the problem caused by
I think the issue is in convert part, when I convert i to string using stringstream and after to QString. May be the value is somehow lost. I am not sure.
QESTION:
How to make the loop accept the convert part?
What do I do wrong in convert part?
In Qt code, you shouldn't be using std::stringstream or std::string. It's pointless.
You have a crashing bug by having the menu on the stack and giving it a parent. It'll be double-destructed.
Don't use the synchronous blocking methods like exec(). Show the menu asynchronously using popup().
In order to react to the actions, connect a slot to the menu's triggered(QAction*) signal. That way you can deal with arbitrary number of automatically generated actions.
You can use the Qt property system to mark actions with custom attributes. QAction is a QObject after all, with all the benefits. For example, you can store your index in an "index" property. It's a dynamic property, created on the fly.
Here's a complete example of how to do it.
main.cpp
#include <QApplication>
#include <QAction>
#include <QMenu>
#include <QDebug>
#include <QPushButton>
struct KMean {
int getK() const { return 3; }
};
class Widget : public QPushButton
{
Q_OBJECT
KMean kmean;
Q_SLOT void triggered(QAction* an) {
const QVariant index(an->property("index"));
if (!index.isValid()) return;
const int i = index.toInt();
setText(QString("Clicked %1").arg(i));
}
Q_SLOT void on_clicked() {
QMenu * menu = new QMenu();
int last = kmean.getK();
for(int i = 1; i <= last; i++)
{
QAction * action = new QAction(QString("Merge with %1").arg(i), menu);
action->setProperty("index", i);
menu->addAction(action);
}
connect(menu, SIGNAL(triggered(QAction*)), SLOT(triggered(QAction*)));
menu->popup(mapToGlobal(rect().bottomRight()));
}
public:
Widget(QWidget *parent = 0) : QPushButton("Show Menu ...", parent) {
connect(this, SIGNAL(clicked()), SLOT(on_clicked()));
}
};
int main (int argc, char **argv)
{
QApplication app(argc, argv);
Widget w;
w.show();
return app.exec();
}
#include "main.moc"

Qt - Access a checkbox in a table

I have a table and each row in the table has a checkbox in it's first column. I need to make it so I can detect which checkboxes are checked and delete those rows when a button is pressed.
QWidget * chkWidget = new QWidget();
QHBoxLayout *center = new QHBoxLayout();
center->setAlignment( Qt::AlignCenter );
center->addWidget( new QCheckBox );
chkWidget->setLayout( center );
ui->data_table->setCellWidget(rowCount,0, chkWidget);
Was this done right? If so how do I access the checkboxes at each row?
I talk about a QTableWidget. You can use a QList.You save your QCheckBox into this QList and use it, when there is some change
Maybe you should check out the documentation
QTableWidget: Link
QList: Link
Here is a solution. I cannot run it at the moment, so please tell me if it works. Please validate the row value. I am not sure if it's possible, that row can have the value -1 when you delete the last row ;)
#include "TestTableWidget.h"
#include "ui_TestTableWidget.h"
TestTableWidget::TestTableWidget(QWidget *parent) : QMainWindow(parent), ui(new Ui::TestTableWidget)
{
ui->setupUi(this);
tableWidget = new QTableWidget(this);
tableWidget->setColumnCount(1); // Just an example
ui->gridLayout->addWidget(tableWidget);
connect(tableWidget, SIGNAL(itemSelectionChanged()), this, SLOT(slotChange()));
for(int i = 1; i < 10; i++)
{
addRow("Row " + QString::number(i));
}
}
TestTableWidget::~TestTableWidget()
{
delete ui;
}
void TestTableWidget::addRow(QString text)
{
int row = tableWidget->rowCount();
qDebug() << "Current row count is " + QString::number(row);
// Add new one
QTableWidgetItem *item = new QTableWidgetItem(text);
tableWidget->insertRow(row);
tableWidget->setItem(row, 0, item);
// Add item to our list
}
void TestTableWidget::slotChange()
{
int row = tableWidget->currentRow();
qDebug() << "Change in table. Current row-index: " + QString::number(row);
// This value is zero-based, so you can use it in our list
}

QTableWidget - setCellWidget missing addition?

I am just trying to add widgets into my table widget and I am trying the code below but all the time I run the program, the first widget is added but the rest is not added. Can you please help me for this situation ?
if(req.at(index).request.CodedValue.size() > 1 )
{
int rowNumber = -1;
for ( int paramNumber = 0 ; paramNumber < req.at(index).request.params.size(); paramNumber++)
{
if(req[index].request.params[paramNumber].semantic == "DATA")
{
rowNumber++;
QComboBox* reqComboBox = new QComboBox();
QLineEdit* tableReqLineEdit = new QLineEdit();
for ( int codedCounter = 0; codedCounter < req.at(index).request.CodedValue.at(paramNumber).trams.size(); codedCounter++)
{
// you should look for the subfunctions and add according to them
reqComboBox->addItem((req[index].request.CodedValue[paramNumber].trams[codedCounter].valueName));
QObject::connect(reqComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(on_tableCombobox_currentIndex());
}
ui.tableWidget->setCellWidget(rowNumber,1,reqComboBox);
}
}
}
Use qDebug in order to see how many times the for loop is executed. Probably it is executed only once:
#include <QDebug>
...
rowNumber++;
qDebug() << rowNumber;
...
Try the following:
for (int i=0; i<ui.tableWidget->rowCount(); i++)
{
ui.tableWidget->setCellWidget(i,1,new QLineEdit);
}
How many line edits do you see?
Notice that you should use the setRowCount in order to set the number of rows of your table widget.