Mahi
Mahi

Reputation: 21883

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

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?

Upvotes: 0

Views: 1248

Answers (1)

Scheff's Cat
Scheff's Cat

Reputation: 20141

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

Snapshot of testQDeleteFromLayoutShift

After clicking on button “Widget 8”:

Delete button "Widget 8"
Destroyed: QPushButton(0x25521e3ef10) "Widget 8"
qGrid.parent().children().count(): 15
qGrid.count(): 14

Snapshot of testQDeleteFromLayoutShift after clicking on button "Widget 8"

After clicking on button “Widget 6”

Delete button "Widget 6"
Destroyed: QPushButton(0x25521e3f7d0) "Widget 6"
qGrid.parent().children().count(): 14
qGrid.count(): 13

Snapshot of testQDeleteFromLayoutShift after clicking on button "Widget 6"

After clicking on button “Widget 12”

Delete button "Widget 12"
Destroyed: QPushButton(0x25521e46ad0) "Widget 12"
qGrid.parent().children().count(): 13
qGrid.count(): 12

Snapshot of testQDeleteFromLayoutShift after clicking on button "Widget 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.

Upvotes: 1

Related Questions