\n
This is the first iteration of an internal tool, so it is not polished, and neither did I separate controller and view of the UI. This is a minimal full example:
\nimport sys\nfrom typing import Any\nfrom typing import List\nfrom typing import Union\n\nfrom PySide6.QtCore import QAbstractItemModel\nfrom PySide6.QtCore import QAbstractListModel\nfrom PySide6.QtCore import QModelIndex\nfrom PySide6.QtCore import QPersistentModelIndex\nfrom PySide6.QtCore import QPoint\nfrom PySide6.QtGui import Qt\nfrom PySide6.QtWidgets import QApplication\nfrom PySide6.QtWidgets import QHBoxLayout\nfrom PySide6.QtWidgets import QListView\nfrom PySide6.QtWidgets import QListWidget\nfrom PySide6.QtWidgets import QMainWindow\nfrom PySide6.QtWidgets import QMenu\nfrom PySide6.QtWidgets import QPushButton\nfrom PySide6.QtWidgets import QVBoxLayout\nfrom PySide6.QtWidgets import QWidget\n\n\nclass PostListModel(QAbstractListModel):\n def __init__(self, full_list: List[str], prefix: str):\n super().__init__()\n self.full_list = full_list\n self.prefix = prefix\n self.endResetModel()\n\n def endResetModel(self) -> None:\n super().endResetModel()\n self.my_list = [\n element for element in self.full_list if element.startswith(self.prefix)\n ]\n\n def rowCount(self, parent=None) -> int:\n return len(self.my_list)\n\n def data(\n self, index: Union[QModelIndex, QPersistentModelIndex], role: int = None\n ) -> Any:\n if role == Qt.DisplayRole or role == Qt.ItemDataRole:\n return self.my_list[index.row()]\n\n\nclass MainWindow(QMainWindow):\n def __init__(self):\n super().__init__()\n self.resize(1200, 500)\n\n prefixes = ["Left", "Center", "Right"]\n self.full_list = [f"{prefix} {i}" for i in range(10) for prefix in prefixes]\n\n central_widget = QWidget(parent=self)\n self.setCentralWidget(central_widget)\n main_layout = QVBoxLayout(parent=central_widget)\n central_widget.setLayout(main_layout)\n columns_layout = QHBoxLayout(parent=main_layout)\n main_layout.addLayout(columns_layout)\n\n self.list_models = {}\n self.list_views = {}\n for prefix in prefixes:\n list_view = QListView(parent=central_widget)\n list_model = PostListModel(self.full_list, prefix)\n self.list_models[prefix] = list_model\n self.list_views[prefix] = list_view\n list_view.setModel(list_model)\n list_view.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)\n list_view.customContextMenuRequested.connect(\n lambda pos: self.show_context_menu(list_view, pos)\n )\n columns_layout.addWidget(list_view)\n print("Created:", list_view)\n\n def show_context_menu(self, list_view, pos: QPoint):\n print("Context menu on:", list_view)\n global_pos = list_view.mapToGlobal(pos)\n index_in_model = list_view.indexAt(pos).row()\n element = list_view.model().my_list[index_in_model]\n\n menu = QMenu()\n menu.addAction("Edit", lambda: print(element))\n menu.exec(global_pos)\n\n\ndef main():\n app = QApplication(sys.argv)\n window = MainWindow()\n window.show()\n retval = app.exec()\n sys.exit(retval)\n\n\nif __name__ == "__main__":\n main()\n
\nWhen it is run like that, it gives this output:
\nCreated: <PySide6.QtWidgets.QListView(0x55f2dcc5c980) at 0x7f1d7cc9d780>\nCreated: <PySide6.QtWidgets.QListView(0x55f2dcc64e10) at 0x7f1d7cc9da40>\nCreated: <PySide6.QtWidgets.QListView(0x55f2dcc6bbe0) at 0x7f1d7cc9dd00>\nContext menu on: <PySide6.QtWidgets.QListView(0x55f2dcc6bbe0) at 0x7f1d7cc9dd00>\n
\nI think that the issue is somewhere in the binding of the lambda to the signal. It seems to always take the last value. Since I re-assign list_view
, I don't think that the reference within the lambda would change.
Something is wrong with the references, but I cannot see it. Do you see why the lambda connected to the context menu signal always has the last list view as context?
\n","author":{"@type":"Person","name":"Martin Ueding"},"upvoteCount":1,"answerCount":1,"acceptedAnswer":{"@type":"Answer","text":"This is happening because lambdas are not immutable objects. All non parameter variables used inside of a lambda will update whenever that variable updates.
\nSo when you connect to the slot:
\nlambda pos: self.show_context_menu(list_view, pos)\n
\nOn each iteration of your for loop you are setting the current value of the list_view variable as the first argument of the show_context_menu
method. But when the list_view changes for the second and proceeding iterations, it is also updating for all the preceding iterations.
Here is a really simple example:
\nnames = ["alice", "bob", "chris"]\n\nfuncs = []\n\nfor name in names:\n funcs.append(lambda: print(f"Hello {name}"))\n\nfor func in funcs:\n func()\n
\nOutput:
\nHello chris\nHello chris\nHello chris\n
\n","author":{"@type":"Person","name":"Alexander"},"upvoteCount":2}}}Reputation: 8727
I write a PySide6 application where I have a layout with three QListView widgets next to each other. Each displays something with a different list model, and all shall have a context menu. What doesn't work is that the right list model or list view gets resolved. This leads to the context menu appearing in the wrong location, and also the context menu actions working on the wrong thing.
I have done a right-click into the circled area, the context menu shows up in the right panel:
This is the first iteration of an internal tool, so it is not polished, and neither did I separate controller and view of the UI. This is a minimal full example:
import sys
from typing import Any
from typing import List
from typing import Union
from PySide6.QtCore import QAbstractItemModel
from PySide6.QtCore import QAbstractListModel
from PySide6.QtCore import QModelIndex
from PySide6.QtCore import QPersistentModelIndex
from PySide6.QtCore import QPoint
from PySide6.QtGui import Qt
from PySide6.QtWidgets import QApplication
from PySide6.QtWidgets import QHBoxLayout
from PySide6.QtWidgets import QListView
from PySide6.QtWidgets import QListWidget
from PySide6.QtWidgets import QMainWindow
from PySide6.QtWidgets import QMenu
from PySide6.QtWidgets import QPushButton
from PySide6.QtWidgets import QVBoxLayout
from PySide6.QtWidgets import QWidget
class PostListModel(QAbstractListModel):
def __init__(self, full_list: List[str], prefix: str):
super().__init__()
self.full_list = full_list
self.prefix = prefix
self.endResetModel()
def endResetModel(self) -> None:
super().endResetModel()
self.my_list = [
element for element in self.full_list if element.startswith(self.prefix)
]
def rowCount(self, parent=None) -> int:
return len(self.my_list)
def data(
self, index: Union[QModelIndex, QPersistentModelIndex], role: int = None
) -> Any:
if role == Qt.DisplayRole or role == Qt.ItemDataRole:
return self.my_list[index.row()]
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.resize(1200, 500)
prefixes = ["Left", "Center", "Right"]
self.full_list = [f"{prefix} {i}" for i in range(10) for prefix in prefixes]
central_widget = QWidget(parent=self)
self.setCentralWidget(central_widget)
main_layout = QVBoxLayout(parent=central_widget)
central_widget.setLayout(main_layout)
columns_layout = QHBoxLayout(parent=main_layout)
main_layout.addLayout(columns_layout)
self.list_models = {}
self.list_views = {}
for prefix in prefixes:
list_view = QListView(parent=central_widget)
list_model = PostListModel(self.full_list, prefix)
self.list_models[prefix] = list_model
self.list_views[prefix] = list_view
list_view.setModel(list_model)
list_view.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
list_view.customContextMenuRequested.connect(
lambda pos: self.show_context_menu(list_view, pos)
)
columns_layout.addWidget(list_view)
print("Created:", list_view)
def show_context_menu(self, list_view, pos: QPoint):
print("Context menu on:", list_view)
global_pos = list_view.mapToGlobal(pos)
index_in_model = list_view.indexAt(pos).row()
element = list_view.model().my_list[index_in_model]
menu = QMenu()
menu.addAction("Edit", lambda: print(element))
menu.exec(global_pos)
def main():
app = QApplication(sys.argv)
window = MainWindow()
window.show()
retval = app.exec()
sys.exit(retval)
if __name__ == "__main__":
main()
When it is run like that, it gives this output:
Created: <PySide6.QtWidgets.QListView(0x55f2dcc5c980) at 0x7f1d7cc9d780>
Created: <PySide6.QtWidgets.QListView(0x55f2dcc64e10) at 0x7f1d7cc9da40>
Created: <PySide6.QtWidgets.QListView(0x55f2dcc6bbe0) at 0x7f1d7cc9dd00>
Context menu on: <PySide6.QtWidgets.QListView(0x55f2dcc6bbe0) at 0x7f1d7cc9dd00>
I think that the issue is somewhere in the binding of the lambda to the signal. It seems to always take the last value. Since I re-assign list_view
, I don't think that the reference within the lambda would change.
Something is wrong with the references, but I cannot see it. Do you see why the lambda connected to the context menu signal always has the last list view as context?
Upvotes: 1
Views: 289
Reputation: 17355
This is happening because lambdas are not immutable objects. All non parameter variables used inside of a lambda will update whenever that variable updates.
So when you connect to the slot:
lambda pos: self.show_context_menu(list_view, pos)
On each iteration of your for loop you are setting the current value of the list_view variable as the first argument of the show_context_menu
method. But when the list_view changes for the second and proceeding iterations, it is also updating for all the preceding iterations.
Here is a really simple example:
names = ["alice", "bob", "chris"]
funcs = []
for name in names:
funcs.append(lambda: print(f"Hello {name}"))
for func in funcs:
func()
Output:
Hello chris
Hello chris
Hello chris
Upvotes: 2