Find out the widget, which is in nth position of the QGridlayout in PyqQt5?

Question:

My intention is to find out the widget’s "n"th position in QGridlayout. In My code, I have three QGridlayout. First QGirdlayout has 4 widgets, the second QGridlayout has 5 widgets and the third QGridlayout has also 5 Widgets. In each QGridlayout, widgets are placed in various positions. How to find out the widget position which is placed in the last or "n"th position(last column, last widget). For Example in the first grid layout, the Lastly Positioned widget is "Button_3", in the second QGridLayout lastly positioned is "Button_9" and the third QGridlayout lastly positioned widget is "Button_14"

enter image description here

from PyQt5.QtWidgets import  *

class Widget(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("QGridlayout")
        self.btn1 = QPushButton("Button_1")
        self.btn2 = QPushButton("Button_2")
        self.btn3 = QPushButton("Button_3")
        self.btn4 = QPushButton("Button_4")
        self.btn4.setSizePolicy(QSizePolicy.Minimum,QSizePolicy.MinimumExpanding)
        self.btn5 = QPushButton("Button_5")
        self.btn6 = QPushButton("Button_6")
        self.btn7 = QPushButton("Button_7")
        self.btn8 = QPushButton("Button_8")
        self.btn9 = QPushButton("Button_9")
        self.btn10 = QPushButton("Button_10")
        self.btn11= QPushButton("Button_11")
        self.btn12 = QPushButton("Button_12")
        self.btn13 = QPushButton("Button_13")
        self.btn14 = QPushButton("Button_14")

        self.gl_1 = QGridLayout()
        self.gl_2 = QGridLayout()
        self.gl_3 = QGridLayout()

        self.vl_1 = QVBoxLayout()


        self.gl_1.addWidget(self.btn1,0,0,1,1)
        self.gl_1.addWidget(self.btn2,0,1,1,1)
        self.gl_1.addWidget(self.btn3,0,2,1,1)
        self.gl_1.addWidget(self.btn4,1,0,1,1)
        self.gl_1.setRowStretch(self.gl_1.rowCount(), 1)
        self.gl_1.setColumnStretch(self.gl_1.columnCount(), 1)
        self.gl_1.setSpacing(1)

        self.gl_2.addWidget(self.btn5,0,0,1,1)
        self.gl_2.addWidget(self.btn6,0,1,1,1)
        self.gl_2.addWidget(self.btn7,1,0,1,1)
        self.gl_2.addWidget(self.btn8,1,1,1,1)
        self.gl_2.addWidget(self.btn9,1,2,1,1)
        self.gl_2.setRowStretch(self.gl_2.rowCount(), 1)
        self.gl_2.setColumnStretch(self.gl_2.columnCount(), 1)
        self.gl_2.setSpacing(1)

        self.gl_3.addWidget(self.btn10, 0, 0, 1, 1)
        self.gl_3.addWidget(self.btn11, 1, 0, 1, 1)
        self.gl_3.addWidget(self.btn12, 1, 1, 1, 1)
        self.gl_3.addWidget(self.btn13, 2, 0, 1, 1)
        self.gl_3.addWidget(self.btn14, 2, 1, 1, 1)
        self.gl_3.setRowStretch(self.gl_3.rowCount(), 1)
        self.gl_3.setColumnStretch(self.gl_3.columnCount(), 1)
        self.gl_3.setSpacing(1)

        self.vl_1.addLayout(self.gl_1)
        self.vl_1.addLayout(self.gl_2)
        self.vl_1.addLayout(self.gl_3)

        self.setLayout(self.vl_1)

if __name__ == '__main__':
    import sys
    app = QApplication(sys.argv)
    w = Widget()
    w.show()
    sys.exit(app.exec_())
Asked By: Bala

||

Answers:

Layout items are always stored internally in a one-dimensional array. They can be accessed using itemAt(), no matter the way the manager lays out its items.

For one dimensional layouts (QBoxLayout, QStackedLayout), though, the order of those items is always consistent with the visual representation; using insertWidget() will change the order of items, so this would always be valid:

item = layout.itemAt(layout.count() - 1)
if isinstance(item.widget(), QWidget):
    print(item.widget())

This also works for grid layouts, as long as the insertion order is consistent with the wanted result.

The common convention follows the standard writing system (the insertion is left to right, then top to bottom), which is that the last object in a "grid" is the rightmost object in the last line (the opposite order above): the last row has precedence over the last column. In the provided code, and following that convention, the last widget of the first layout is "Button_4" (no matter the insertion order).

In your case, though, it won’t work: while the insertion order follows that common convention, your request is the opposite: the rightmost column has precedence over the bottom row, similarly to some eastern writing systems (not Japanese or Chinese).

Also, if program is able to add and remove widgets dynamically, the insertion order becomes inconsistent for QGridLayout.

There are two possible solutions for this, and the choice depends on the actual program needs.

To explain this, I’ve modified the original code to make it more readable and understandable; I also randomized the order of insertion in order to explain what written above.

from random import sample
from PyQt5.QtWidgets import  *

grids = [
    {
        1: (0, 0), 
        2: (0, 1), 
        3: (0, 2), 
        4: (1, 0), 
    }, 
    {
        5: (0, 0), 
        6: (0, 1), 
        7: (1, 0), 
        8: (1, 1), 
        9: (1, 2), 
    }, 
    {
        10: (0, 0), 
        11: (1, 0), 
        12: (1, 1), 
        13: (2, 0), 
        14: (2, 1), 
    }
]

class Widget(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("QGridlayout")
        mainLayout = QVBoxLayout(self)

        for data in grids:
            grid = QGridLayout()
            for key in sample(data.keys(), k=len(data.keys())):
                button = QPushButton('Button_{}'.format(key))
                grid.addWidget(button, *data[key])
                button.clicked.connect(
                    lambda _, grid=grid: self.getLastWidget(grid))
            grid.setColumnStretch(grid.columnCount(), 1)
            grid.setSpacing(1)
            mainLayout.addLayout(grid)

    def getLastWidget(self, layout):
        # see implementations below
        pass


if __name__ == '__main__':
    import sys
    app = QApplication(sys.argv)
    w = Widget()
    w.show()
    sys.exit(app.exec_())

Iterate through all items

For this, we iterate through all items of the layout, and look for the latest item based on the precedence above: first, check if it’s the last column, then if it’s the last row.

In order to do so, we obviously need to work with getItemPosition(). Then we compute the actual row/column (considering spanning), check if the column is equal or greater than the previous "maximum column" and eventually also check if the row is equal or greater than the previous "maximum row".

    def getLastWidget(self, layout):
        count = layout.count()
        for i in range(count - 1, -1, -1):
            item = layout.itemAt(i)
            if isinstance(item.widget(), QPushButton):
                print('last button in layout: {}'.format(
                    item.widget().text()))
                break

        lastRow = lastColumn = -1
        lastButton = None
        for i in range(layout.count()):
            row, column, rowSpan, colSpan = layout.getItemPosition(i)
            row += rowSpan - 1
            column += colSpan - 1
            if column >= lastColumn:
                item = layout.itemAt(i)
                if row >= lastRow and isinstance(item.widget(), QPushButton):
                    lastButton = item.widget()
                    if column > lastColumn:
                        lastRow = -1
                    else:
                        lastRow = row
                lastColumn = column 

        if lastButton:
            print('last button of the last column: {}'.format(
                lastButton.text()))

Check for the last row/column of the grid

For this we do the opposite, starting from the last row and column.

It’s important to remember that the row and column count of a grid layout is not reliable (see this related post), so we also need to ensure that an item actually exists at a given row and column.

    def getLastWidget(self, layout):
        rows = layout.rowCount()
        columns = layout.columnCount()
        rowRange = range(rows - 1, -1, -1)
        for column in range(columns - 1, -1, -1):
            for row in rowRange:
                item = layout.itemAtPosition(row, column)
                if (
                    item is not None
                    and isinstance(item.widget(), QPushButton)
                ):
                    break
            else:
                continue
            break
        else:
            return
        print(item.widget().text())

Final considerations

The choice from the above approaches completely depends on the final purpose and behavior of the program.

Most importantly, be aware that the last approach uses a function that is clearly simpler in form, but its simplicity only works in simple cases and as long as dynamic adding of widgets is done carefully.

For instance, if the program is intended to be running for a long time, with continuous updates to the layout manager, it’s easy to end up with a grid layout that has tens or hundreds of empty rows and columns, making the above simplicity completely pointless.

This is clearly the case of using functions that add widgets or set stretch or minimum widths/heights using the "current" layout row or column count (as done in the OP code), and this is related to the unreliable count noted above.

It’s easy to lose track of the grid size, even when we actually have just a few widgets, and in that case the first approach would return almost instantly, while the second could require seconds to return.

IF the layout is dynamical and its changes are properly done, the last approach is obviously the better one; otherwise, the first one is certainly the safer.

Answered By: musicamante
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.