nKn nKn - 5 months ago 16
Python Question

Setting event's parameter value at declaration time in PyQT5?

Background



I'm writing a simple application in PyQT5. The main
QWidget
is a
QGridLayout
, and visually the result should be something like this:

+-----------------------------------+
row1 | QLabelA | QLabelB | QLabelC |
+-----------------------------------+
row2 | QLabelA | QLabelB | QLabelC |
+-----------------------------------+
row3 | QLabelA | QLabelB | QLabelC |
+-----------------------------------+
...


This
QGridLayout
is filled using a loop where each iteration represents a row, so I have something like this:

row = 0
self.grid = QGridLayout()

while row < 10:
filepath = 'icon.png'
icon = QImage(filepath)
QLabelA = QLabel()
QLabelA.setPixmap(QPixmap.fromImage(icon))

QLabelB = QLabel()
QLabelB.setPixmap(QPixmap.fromImage(icon))

QLabelC = QLabel()
QLabelC.setPixmap(QPixmap.fromImage(icon))

self.grid.addWidget(QLabelA, row, 0)
self.grid.addWidget(QLabelB, row, 1)
self.grid.addWidget(QLabelC, row, 2)

row += 1


So far, this works quite well.

The problem



My aim is to make that each of these
QLabel
s, which visually are images, have an associated action that should be triggered after a user's simple click over each of them. For that, I found a property that allows calling a method after clicking over an object, so I could simply do this:

QLabelA.mousePressedEvent = this.do_something

def do_something(self, event):
...


The first problem is easy to see: I need a method to identify which of the
QLabel
s has been clicked (i.e., which row). For that, I found a way to add custom parameters to the
mousePressedEvent
. Let's focus on the
QLabelA
fields which will call the
clicked()
method on click:

QLabelA.mousePressEvent = lambda x: self.clicked(row)

def clicked(self, row):
print row


Here comes the big problem: Each of these
QLabelA
fields are dependent on the
row
variable and they are not evaluated at creation time, but at click time instead. Thus, if the user clicks on
QLabelA
at row 0, inside the
clicked()
method,
row
will have the last
row
value (i.e., the number of rows of the
QGridLayout
) instead of the one that had at definition time. If
self.grid
has 5 rows, it doesn't matter where the user clicks,
row
will always have a value of 5.

I tried workarounding this limitation by defining a list with the
row
value inside, but it made no difference since it is still dependent on the
row
value at click time.

Question



Is there a way to make this work as intended and assign the value passed to the
click()
method at creation time instead of click time? Any different approach or way to achieve this are also welcome.

Answer

You're rather close to a solution with your lambda x: statement, you're just missing one little piece

Using Lambdas to store transient values

Since your row variable changes a lot, you need to store the value when the function is created; your self.clicked references the current row, as you notice yourself. To help that, you can change your lambda like so:

# current: x is the event, row is looked up when the function is called
lambda x: self.clicked(row)

# New: x is the event, r is frozen when the function is created
lambda x, r=row: self.clicked(r)

This causes each lambda to have a local r set at creation-time rather than looking up row when the function is needed.