QListView context menu gets wrong context - python

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?

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

Related

PyQt5 application: button press event not working [duplicate]

Hopefully I am following the guidelines correctly here with my first question. I am trying to create a GUI with the MVC structure. I am having difficulty with understanding why my signals are not always being picked up by the controller. I know that there is just something simple that I'm missing. I'm attaching code from a simple calculator which I used as a guide. I removed most of the features to simplify this as much as possible. It is now only 3 of the original buttons and my own button. For debugging, I just have the value on the button printed out when you press it.
import sys
# Import QApplication and the required widgets from PyQt5.QtWidgets
from PySide2.QtWidgets import QApplication
from PySide2.QtWidgets import QMainWindow
from PySide2.QtWidgets import QWidget
from PySide2.QtCore import Qt
from PySide2.QtWidgets import QGridLayout
from PySide2.QtWidgets import QLineEdit
from PySide2.QtWidgets import QPushButton
from PySide2.QtWidgets import QVBoxLayout
from functools import partial
ERROR_MSG = 'ERROR'
# Create a subclass of QMainWindow to setup the calculator's GUI
class PyCalcUi(QMainWindow):
"""PyCalc's View (GUI)."""
def __init__(self):
"""View initializer."""
super().__init__()
# Set some main window's properties
self.setWindowTitle('PyCalc')
self.setFixedSize(235, 235)
# Set the central widget and the general layout
self.generalLayout = QVBoxLayout()
self._centralWidget = QWidget(self)
self.setCentralWidget(self._centralWidget)
self._centralWidget.setLayout(self.generalLayout)
# Create the display and the buttons
self._createDisplay()
self._createButtons()
def _createDisplay(self):
"""Create the display."""
# Create the display widget
self.display = QLineEdit()
# Set some display's properties
self.display.setFixedHeight(35)
self.display.setAlignment(Qt.AlignRight)
self.display.setReadOnly(True)
# Add the display to the general layout
self.generalLayout.addWidget(self.display)
def _createButtons(self):
"""Create the buttons."""
self.buttons = {}
buttonsLayout = QGridLayout()
# Button text | position on the QGridLayout
buttons = {'7': (0, 0),
'8': (0, 1),
'9': (0, 2),
}
# Create the buttons and add them to the grid layout
for btnText, pos in buttons.items():
self.buttons[btnText] = QPushButton(btnText)
self.buttons[btnText].setFixedSize(40, 40)
buttonsLayout.addWidget(self.buttons[btnText], pos[0], pos[1])
self.mybutton = QPushButton("5")
buttonsLayout.addWidget(self.mybutton,1,0)
# Add buttonsLayout to the general layout
self.generalLayout.addLayout(buttonsLayout)
# Create a Controller class to connect the GUI and the model
class PyCalcCtrl:
"""PyCalc Controller class."""
def __init__(self, model, view):
"""Controller initializer."""
self._evaluate = model
self._view = view
# Connect signals and slots
self._connectSignals()
def _printthis(self):
print("Hi")
def _printthat(self, buttonvalue):
print(buttonvalue)
def _connectSignals(self):
"""Connect signals and slots."""
self._view.mybutton.clicked.connect(self._printthis)
for btnText, btn in self._view.buttons.items():
btn.clicked.connect(partial(self._printthat, btnText))
# Create a Model to handle the calculator's operation
def evaluateExpression(expression):
"""Evaluate an expression."""
try:
result = str(eval(expression, {}, {}))
except Exception:
result = ERROR_MSG
return result
# Client code
def main():
"""Main function."""
# Create an instance of QApplication if it doesn't exist
pycalc = QApplication.instance()
if pycalc is None:
pycalc = QApplication(sys.argv)
# Show the calculator's GUI
view = PyCalcUi()
view.show()
# Create instances of the model and the controller
model = evaluateExpression
PyCalcCtrl(model=model, view=view)
# Execute the calculator's main loop
sys.exit(pycalc.exec_())
if __name__ == '__main__':
main()
This set of code works, BUT if I comment out the
for btnText, btn in self._view.buttons.items():
btn.clicked.connect(partial(self._printthat, btnText))
The self._view.mybutton.clicked.connect(self._printthis) will no longer work.
What is the btn.clicked.connect(partial(self._printthat, btnText)) line doing which is allowing any other signal I put in def _connectSignals(self): to work. What aspect of that line is achieving something that the mybutton signal isn't doing?
The problem is caused because the PyCalcCtrl object is not assigned to a variable so it will be destroyed and therefore the "_printthis" method will not be accessible. On the other hand, when functools.partial is used then the object of the PyCalcCtrl class is assigned to the scope of that function, that's why it works.
The solution is to assign the PyCalcCtrl object to a variable:
ctrl = PyCalcCtrl(model=model, view=view)

Pass in dynamic widget object to QVariantAnimation valueChanged signal handler

I'm trying to make an invalid animation tool. I have a button that when pressed, animates the background color of a field from red to the "normal" field color. This works great but now I want to pass in any arbitrary PyQt widget object (could be QLineEdit, QComboBox, and so on). Here's what my animation handler looks like:
#QtCore.pyqtSlot(QtGui.QColor)
def invalid_animation_handler(color: QtGui.QColor) -> None:
field.setStyleSheet(f"background-color: {QtGui.QColor(color).name()}")
Currently it requires that the field object be already named before calling the function to change the background. I would like to be able to dynamically pass in a widget and set the stylesheet on the fly by passing in a parameter, something like this:
#QtCore.pyqtSlot(QtGui.QColor)
def invalid_animation_handler(widget, color: QtGui.QColor) -> None:
widget.setStyleSheet(f"background-color: {QtGui.QColor(color).name()}")
When I try to do this, the widget is able to be passed but the constant color change from WARNING_COLOR to NORMAL_COLOR doesn't work anymore. I also do not want to put this into a class since it has to be on-the-fly. My goal is being able to call a function to start the animation from anywhere instead of having to press the button. The desired goal is like this:
class VariantAnimation(QtCore.QVariantAnimation):
"""VariantAnimation: Implement method for QVariantAnimation to fix pure virtual method in PyQt5 -> PyQt4"""
def updateCurrentValue(self, value):
pass
#QtCore.pyqtSlot(QtGui.QColor)
def invalid_animation_handler(widget, color: QtGui.QColor) -> None:
widget.setStyleSheet(f"background-color: {QtGui.QColor(color).name()}")
def invalid_animation(widget):
return VariantAnimation(startValue=ERROR_COLOR, endValue=NORMAL_COLOR, duration=ANIMATION_DURATION, valueChanged=lambda: invalid_animation_handler(widget))
def start_invalid_animation(animation_handler) -> None:
if animation_handler.state() == QtCore.QAbstractAnimation.Running:
animation_handler.stop()
animation_handler.start()
field = QtGui.QLineEdit()
field_animation_handler = invalid_animation(field)
# Goal is to make a generic handler to start the animation
start_invalid_animation(field_animation_handler)
Minimal working example
import sys
from PyQt4 import QtCore, QtGui
class VariantAnimation(QtCore.QVariantAnimation):
"""VariantAnimation: Implement method for QVariantAnimation to fix pure virtual method in PyQt5 -> PyQt4"""
def updateCurrentValue(self, value):
pass
#QtCore.pyqtSlot(QtGui.QColor)
def invalid_animation_handler(color: QtGui.QColor) -> None:
field.setStyleSheet(f"background-color: {QtGui.QColor(color).name()}")
def start_field_invalid_animation() -> None:
if field_invalid_animation.state() == QtCore.QAbstractAnimation.Running:
field_invalid_animation.stop()
field_invalid_animation.start()
NORMAL_COLOR = QtGui.QColor(25,35,45)
SUCCESSFUL_COLOR = QtGui.QColor(95,186,125)
WARNING_COLOR = QtGui.QColor(251,188,5)
ERROR_COLOR = QtGui.QColor(247,131,128)
ANIMATION_DURATION = 1500
if __name__== '__main__':
app = QtGui.QApplication(sys.argv)
button = QtGui.QPushButton('Animate field background')
button.clicked.connect(start_field_invalid_animation)
field = QtGui.QLineEdit()
field_invalid_animation = VariantAnimation(startValue=ERROR_COLOR, endValue=NORMAL_COLOR, duration=ANIMATION_DURATION, valueChanged=invalid_animation_handler)
mw = QtGui.QMainWindow()
layout = QtGui.QHBoxLayout()
layout.addWidget(button)
layout.addWidget(field)
window = QtGui.QWidget()
window.setLayout(layout)
mw.setCentralWidget(window)
mw.show()
sys.exit(app.exec_())
I do not understand exactly why you do not want a class but IMO is the most suitable solution. The logic is to store the callable that allows to change the property and invoke it in updateCurrentValue.
Currently I don't have PyQt4 installed so I implemented the logic with PyQt5 but I don't think it is difficult to change the imports.
import sys
from dataclasses import dataclass
from functools import partial
from typing import Callable
from PyQt5.QtCore import QAbstractAnimation, QObject, QVariant, QVariantAnimation
from PyQt5.QtGui import QColor
from PyQt5.QtWidgets import (
QApplication,
QHBoxLayout,
QLineEdit,
QMainWindow,
QPushButton,
QWidget,
)
#dataclass
class VariantAnimation(QVariantAnimation):
widget: QWidget
callback: Callable[[QWidget, QVariant], None]
start_value: QVariant
end_value: QVariant
duration: int
parent: QObject = None
def __post_init__(self) -> None:
super().__init__()
self.setStartValue(self.start_value)
self.setEndValue(self.end_value)
self.setDuration(self.duration)
self.setParent(self.parent)
def updateCurrentValue(self, value):
if isinstance(self.widget, QWidget) and callable(self.callback):
self.callback(self.widget, value)
def invalid_animation_handler(widget: QWidget, color: QColor) -> None:
widget.setStyleSheet(f"background-color: {QColor(color).name()}")
def start_field_invalid_animation(animation: QAbstractAnimation) -> None:
if animation.state() == QAbstractAnimation.Running:
animation.stop()
animation.start()
NORMAL_COLOR = QColor(25, 35, 45)
SUCCESSFUL_COLOR = QColor(95, 186, 125)
WARNING_COLOR = QColor(251, 188, 5)
ERROR_COLOR = QColor(247, 131, 128)
ANIMATION_DURATION = 1500
if __name__ == "__main__":
app = QApplication(sys.argv)
button = QPushButton("Animate field background")
field = QLineEdit()
animation = VariantAnimation(
widget=field,
callback=invalid_animation_handler,
start_value=ERROR_COLOR,
end_value=NORMAL_COLOR,
duration=ANIMATION_DURATION,
)
button.clicked.connect(partial(start_field_invalid_animation, animation))
mw = QMainWindow()
layout = QHBoxLayout()
layout.addWidget(button)
layout.addWidget(field)
window = QWidget()
window.setLayout(layout)
mw.setCentralWidget(window)
mw.show()
sys.exit(app.exec_())

Qaction Shortcut in extended contextmenu not triggered

I try to extend the contextmenu of a QLineEdit with an additional entry for replacing text. I can extend the contextmenu with .createStandardContextMenu(), which works fine. But when I try to add a shortcut with .setShortcut(QKeySequence(Qt.CTRL + Qt.Key_R)) it will not react on the key. Same with different keys, which I tried. In addition the shortcut made with QAction('&Replace', self) doesn't work too.
Some examples here in SO and other sources are constructed in the same way, so I'm wondering that nobody else has got the same problem. Seems that I'm missing anything. But what? I can't figure out, checked the docs multiple times.
Working Example:
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
import sys
class ECM(QWidget):
def __init__(self):
super(ECM, self).__init__()
self.setWindowTitle("Extended Context Menu")
self.lineEdit = QLineEdit()
self.lineEdit.setContextMenuPolicy(Qt.CustomContextMenu)
self.lineEdit.customContextMenuRequested.connect(self.my_contextMenuEvent)
layout = QVBoxLayout()
layout.addWidget(self.lineEdit)
self.setLayout(layout)
self.setFixedSize(800,200)
self.show()
def replace(self):
print("replace")
def my_contextMenuEvent(self):
print("my_contextMenuEvent")
menu = self.lineEdit.createStandardContextMenu()
action = QAction('&Replace', self)
action.setStatusTip('Replace values')
action.setShortcut(QKeySequence(Qt.CTRL + Qt.Key_R))
action.triggered.connect(self.replace)
menu.addAction(action)
menu.exec_()
if __name__ == '__main__':
app = QApplication(sys.argv)
sender = ECM()
app.exec_()
Based on musicamante's comment I came to the following solution:
Extract from the docs:
If you want to extend the standard context menu, reimplement this
function, call createStandardContextMenu() and extend the menu
returned.
The default use of the QAction list (as returned by actions()) is to
create a context QMenu.
It's not totally logically to me, not for the 1st time ;-)
Final code:
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
import sys
class ECM(QWidget):
def __init__(self):
super(ECM, self).__init__()
self.setWindowTitle("Extended Context Menu")
self.lineEdit = QLineEdit()
self.lineEdit.setContextMenuPolicy(Qt.CustomContextMenu)
self.lineEdit.customContextMenuRequested.connect(self.my_contextMenuEvent)
layout = QVBoxLayout()
layout.addWidget(self.lineEdit)
self.setLayout(layout)
self.setFixedSize(800,200)
action = QAction('&Replace', self)
action.setStatusTip('Replace values')
action.setShortcut(QKeySequence(Qt.CTRL + Qt.Key_R))
action.triggered.connect(self.replace)
self.lineEdit.addAction(action)
self.show()
def replace(self):
print("replace")
def my_contextMenuEvent(self):
menu = self.lineEdit.createStandardContextMenu()
menu.addActions(self.lineEdit.actions())
menu.exec_()
if __name__ == '__main__':
app = QApplication(sys.argv)
sender = ECM()
app.exec_()

Can QCompleter's list of values be modified/updated later again in pyqt5?

I am making a GUI using Pyqt5 where there are 5 Qlineedit fields.
The range of values for each can be say [1 -5]. If the user tried to select third Qlineedit field and selected the value '2'. The other fields range should now not include '2' in it.
Similarly if he selected another field with value '4', the remaining fields should have only [1, 3, 5] as available options.
(Please bear in mind that user can delete the value in any field too and available values should be updated accordingly.)
I tried to use list and update QCompleter for the fields as soon as I see any textChanged signal.
The below code works perfectly except when the user lets say had given the input '14' in some field before and now tries to delete it using backspace. '4' will get deleted but on deleting '1' it will crash without any backtrace.
"Process finished with exit code -1073741819 (0xC0000005)"
If I click on the lineedit field before deleting '1', it works fine.
Minimal code -
#!/usr/bin/env python
import logging
import sys
import traceback
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLineEdit, QApplication, QCompleter
class MyWidget(QWidget):
def __init__(self, parent=None):
super(MyWidget, self).__init__(parent)
vbox = QVBoxLayout(self)
self.setLayout(vbox)
self.names = ['11', '12', '13', '14', '15']
self.names.sort()
self.line_edits_list = [0]*5
for i in range(len(self.names)):
self.line_edits_list[i] = QLineEdit(self)
completer = QCompleter(self.names, self.line_edits_list[i])
self.line_edits_list[i].setCompleter(completer)
vbox.addWidget(self.line_edits_list[i])
self.line_edits_list[i].textEdited.connect(self.text_changed)
def text_changed(self, text):
names_sel = []
# Check if current text matches anything in our list, if it does add it to a new list
for i in range(len(self.line_edits_list)):
if self.line_edits_list[i].text() in self.names and self.line_edits_list[i].text() not in names_sel:
names_sel.append(self.line_edits_list[i].text())
# The remaining textfields should get their qcompleter ranges updated with unique values of the two lists
for i in range(len(self.line_edits_list)):
if self.line_edits_list[i].text() not in self.names:
try:
new_range = list((set(self.names) - set(names_sel)))
completer = QCompleter(new_range, self.line_edits_list[i])
self.line_edits_list[i].setCompleter(completer)
except:
print(traceback.format_exc())
def test():
app = QApplication(sys.argv)
w = MyWidget()
w.show()
app.exec_()
print("END")
if __name__ == '__main__':
test()
As I pointed out in the comments, using QLineEdit is not a suitable widget if you want to restrict the values that the user can select. In this case, as the user has multiple options, then a suitable widget is the QComboBox. To do the filtering you can use QSortFilterProxyModel overriding the filterAcceptsRow method to implement custom logic.
#!/usr/bin/env python
import sys
from PyQt5.QtCore import QSortFilterProxyModel
from PyQt5.QtGui import QStandardItem, QStandardItemModel
from PyQt5.QtWidgets import QApplication, QComboBox, QVBoxLayout, QWidget
class ProxyModel(QSortFilterProxyModel):
def __init__(self, parent=None):
super().__init__(parent)
self._hide_items = []
#property
def hide_items(self):
return self._hide_items
#hide_items.setter
def hide_items(self, items):
self._hide_items = items
self.invalidateFilter()
def filterAcceptsRow(self, sourceRow, sourceParent):
index = self.sourceModel().index(sourceRow, 0, sourceParent)
return index.data() not in self.hide_items
class MyWidget(QWidget):
def __init__(self, names, parent=None):
super(MyWidget, self).__init__(parent)
self._comboboxes = []
model = QStandardItemModel()
vbox = QVBoxLayout(self)
for name in names:
model.appendRow(QStandardItem(name))
proxy_model = ProxyModel()
proxy_model.setSourceModel(model)
combobox = QComboBox()
combobox.setModel(proxy_model)
vbox.addWidget(combobox)
combobox.setCurrentIndex(-1)
combobox.currentIndexChanged.connect(self.handle_currentIndexChanged)
self.comboboxes.append(combobox)
self.resize(640, 480)
#property
def comboboxes(self):
return self._comboboxes
def handle_currentIndexChanged(self):
rows = []
for combobox in self.comboboxes:
if combobox.currentIndex() != -1:
rows.append(combobox.currentText())
for i, combobox in enumerate(self.comboboxes):
index = combobox.currentIndex()
proxy_model = combobox.model()
r = rows[:]
if index != -1:
text = combobox.currentText()
if text in r:
r.remove(text)
combobox.blockSignals(True)
proxy_model.hide_items = r
combobox.blockSignals(False)
def test():
app = QApplication(sys.argv)
names = ["11", "12", "13", "14", "15"]
w = MyWidget(names)
w.show()
app.exec_()
if __name__ == "__main__":
test()
The correct way to implement this is by using a model with the QCompleter. The model can change its content over time and the completer will react to it accordingly.
In your case, you could create a model that contains all possible values first. Then, you could use a QSortFilterProxyModel, which, given you current UI state, could reduce the set. The proxy model is the one that you use with QCompleter.
This (otherwise unrelated) question is example Python code for implementing such a proxy model: QSortFilterProxyModel does not apply Caseinsensitive

context menu in QFileDialog

I would like to implement a custom context menu in a QFileDialog. In the code below, I managed to create a context menu to the main window, but I would like the menu to be displayed when a file is selected : how to know the right widget in the QFileDialog I should apply setContextMenuPolicy ?
import sys
from PyQt4.QtCore import *
from PyQt4.QtGui import *
class Window(QWidget):
def __init__(self):
QWidget.__init__(self)
self.myFileDialog = QFileDialog()
self.myFileDialog.setContextMenuPolicy(Qt.CustomContextMenu)
self.myFileDialog.customContextMenuRequested.connect(self.openMenu)
layout = QVBoxLayout()
layout.addWidget(self.myFileDialog)
self.setLayout(layout)
self.action_perso = QAction( "MyOwnMenu", self )
self.connect( self.action_perso, SIGNAL("triggered()"), self.test )
def openMenu(self, position):
menu = QMenu()
menu.addAction(self.action_perso)
menu.exec_(self.myFileDialog.mapToGlobal(position))
def test(self):
print("coucou")
if __name__ == "__main__":
app = QApplication(sys.argv)
window = Window()
window.show()
sys.exit(app.exec_())
I found a solution, which may not be the best. It relies on two elements:
Thank to a personnal function objectTree (shown here but not used) that lists recursively all child objects of the QFileDialog, I identified the right widget, i.e. the QTreeView (I understood that the QTreeView is the right widget by trying to hide successively all QListView and QTreeView widgets). Hence, I can select it by its objectName with self.findChild(QTreeView, "treeView")
Application of setContextMenuPolicy( Qt.ActionsContextMenu ) to this QTreeView. I have also tried to implement a setContextMenuPolicy(Qt.CustomContextMenu), and it worked partially: my menu did appear but under the origin menu that was not unactivaded !
Below is the code I propose :
import sys
from PyQt4.QtCore import *
from PyQt4.QtGui import *
class CustomWidget(QFileDialog):
def __init__(self, parent=None):
super(CustomWidget,self).__init__(parent)
# fetch the QTreeView in the QFileDialog
self.myTree = self.findChild(QTreeView, "treeView")
# set the context menu policy to ActionsContextMenu
self.myTree.setContextMenuPolicy( Qt.ActionsContextMenu )
# Define a new action
self.action_perso = QAction( "MyOwnMenu", self.myTree )
self.myTree.addAction( self.action_perso )
# connect this action to a personnal function
self.connect( self.action_perso, SIGNAL("triggered()"), self.myFunction )
def myFunction(self):
print("coucou")
def objectTree(self, objet, plan, j):
""" list recursively all child objects of objet to fetch the right widget """
n = len( objet.children() )
for i, child in enumerate( objet.children() ):
#print("\t"*j, end="")
plan_sup = plan+"."+str(i)
#print( plan_sup, child )
if isinstance(child, QTreeView):
self.listViews.append(child)
self.objectTree(child, plan_sup, j+1)
class MainWidget(QWidget):
def __init__(self, parent=None):
super(MainWidget,self).__init__(parent)
#creation of main layout
mainLayout = QVBoxLayout()
# creation of a widget inside
self.monWidget = CustomWidget()
mainLayout.addWidget( self.monWidget )
self.setLayout( mainLayout )
self.show()
app = QApplication(sys.argv)
window = MainWidget()
sys.exit(app.exec_())

Categories