Skip to content
Advertisement

Multiple context menu in a single qTableView pyqt5

i added two functions in two different pushButtons. for both function i used a common qtableView. i created two contextmenu, whenever user clicked pushButton1 func1 is called and result set will be shown in qtableView and user can select items from contextmenu1 and same like pushButton2 ,where contextMenu2 will appear . but in my code context menu is not showing correctly. for ex: if user hit pushbutton1 then for both dataset context menu is same. so context menu is depends on which pushButton hit first. also menu is not close quickly after selected the item ,why!

main.py

from PyQt5 import QtCore, QtWidgets
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QMenu

class TableModel(QtCore.QAbstractTableModel):
    def __init__(self, data):
        super(TableModel, self).__init__()
        self._data = data
    def data(self, index, role):
        if role == Qt.DisplayRole:
            return self._data[index.row()][index.column()]
    def rowCount(self, index):
        return len(self._data)
    def columnCount(self, index):
        return len(self._data[0])


class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(800, 600)
        self.centralwidget = QtWidgets.QWidget(MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.verticalLayout = QtWidgets.QVBoxLayout(self.centralwidget)
        self.verticalLayout.setObjectName("verticalLayout")
        self.frame = QtWidgets.QFrame(self.centralwidget)
        self.frame.setFrameShape(QtWidgets.QFrame.StyledPanel)
        self.frame.setFrameShadow(QtWidgets.QFrame.Raised)
        self.frame.setObjectName("frame")
        self.horizontalLayout = QtWidgets.QHBoxLayout(self.frame)
        self.horizontalLayout.setObjectName("horizontalLayout")
        self.pushButton1 = QtWidgets.QPushButton(self.frame)
        self.pushButton1.setObjectName("pushButton1")
        self.horizontalLayout.addWidget(self.pushButton1)
        self.pushButton_2 = QtWidgets.QPushButton(self.frame)
        self.pushButton_2.setObjectName("pushButton_2")
        self.horizontalLayout.addWidget(self.pushButton_2)
        spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
        self.horizontalLayout.addItem(spacerItem)
        self.verticalLayout.addWidget(self.frame)
        self.tableView = QtWidgets.QTableView(self.centralwidget)
        self.tableView.setObjectName("tableView")
        self.verticalLayout.addWidget(self.tableView)
        MainWindow.setCentralWidget(self.centralwidget)
        self.retranslateUi(MainWindow)
        QtCore.QMetaObject.connectSlotsByName(MainWindow)

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
        self.pushButton1.setText(_translate("MainWindow", "PushButton1"))
        self.pushButton_2.setText(_translate("MainWindow", "PushButton2"))
        self.pushButton1.clicked.connect(self.func1)
        self.pushButton_2.clicked.connect(self.func2)

    def func1(self):
        print('funct1 called')
        data = [
          [4, 9, 2],
          [1, 0, 0],
          [3, 5, 0],
          [3, 3, 2],
          [7, 8, 9],
        ]
        
        self.model = TableModel(data)
        self.tableView.setModel(self.model)
        self.tableView.setContextMenuPolicy(Qt.CustomContextMenu)
        self.tableView.customContextMenuRequested.connect(self.onCustomContextMenuRequested)
       
       
    def func2(self):
        data2 = [
          [0, 0, 0],
          [0, 0, 0],
          [0, 0, 0],
          [0, 0, 0],
          [0, 0, 0],
        ]
        self.model = TableModel(data2)
        self.tableView.setModel(self.model)
        self.tableView.customContextMenuRequested.connect(self.onCustomContextMenuRequested_2)
        
    def onCustomContextMenuRequested(self, pos):
        index = self.tableView.indexAt(pos)
        if index.isValid():
            menu = QMenu()
            CutAction  = menu.addAction("&function 3")
            CutAction2 = menu.addAction("&function 4")
            menu.addAction(CutAction)
            menu.addAction(CutAction2)
            CutAction.triggered.connect(self.function4)
            menu.exec_(self.tableView.viewport().mapToGlobal(pos))
            menu.close()

    def onCustomContextMenuRequested_2(self, pos):
        index = self.tableView.indexAt(pos)
        if index.isValid():
            menu = QMenu()
            CutAction  = menu.addAction("&function 5")
            CutAction2 = menu.addAction("&function 6")
            menu.addAction(CutAction)
            menu.addAction(CutAction2)
            CutAction.triggered.connect(self.function5)
            menu.exec_(self.tableView.viewport().mapToGlobal(pos))
            menu.close()


    def function4(self):
        print("function 4")

    def function5(self):
        print("function 5")
if __name__ == "__main__":
    import sys
    app = QtWidgets.QApplication(sys.argv)
    MainWindow = QtWidgets.QMainWindow()
    ui = Ui_MainWindow()
    ui.setupUi(MainWindow)
    MainWindow.show()
    sys.exit(app.exec_())

Advertisement

Answer

Every signal connection triggers the target slot (function). If you connect the same signal to two functions, both functions get called, even if you connect twice to the same function. In fact, when you say “menu is not close[d] quickly after select[ing] the item”, the truth is that the menu is opened multiple times. You can easily check that if you just add a print statement to each function.

There are different solutions depending on the situation (unfortunately, your code and function namings aren’t very verbose, so we cannot really know what every function does).

Disconnect from all other slots

This can be done by calling a simple disconnect() on the signal, which disconnects from all other (already) connected slots. Note that this would raise an exception if no connection exists, so it should be done inside a try/except block:

    def func1(self):
        # ...
        try:
            self.tableView.customContextMenuRequested.disconnect()
        except TypeError:
            pass
        self.tableView.customContextMenuRequested.connect(self.onCustomContextMenuRequested)

The same goes for func2, obviously.

Disconnect from known slots

Similarly to the previous point, but only disconnects from the other slots.
Be aware that this wouldn’t work well in your case, as you’re connecting with a button click (which could happen more than once, resulting in calling the menu function for every time you clicked the button, as explained above).

Connect to a single slot, with a variable to choose the menu

This is probably the best and safest method for this kind of situations.

Instead of continuously changing the target of the connection, set a variable to specify which model is referenced and use a single function that will decide which menu use based on that variable.

Note that in this case I set the context policy (which can actually be done in Designer) and the connection directly in the setupUi, but in reality none of this should ever be done (more on this later).

class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        # ...
        self.tableView.setContextMenuPolicy(Qt.CustomContextMenu)
        self.tableView.customContextMenuRequested.connect(
            self.onCustomContextMenuRequested)

    def func1(self):
        self.target = 1
        data = [
          [4, 9, 2],
          [1, 0, 0],
          [3, 5, 0],
          [3, 3, 2],
          [7, 8, 9],
        ]
        
        self.model = TableModel(data)
        self.tableView.setModel(self.model)

    def func2(self):
        self.target = 2
        data2 = [
          [0, 0, 0],
          [0, 0, 0],
          [0, 0, 0],
          [0, 0, 0],
          [0, 0, 0],
        ]
        self.model = TableModel(data2)
        self.tableView.setModel(self.model)
        
    def onCustomContextMenuRequested(self, pos):
        index = self.tableView.indexAt(pos)
        if not index.isValid():
            return

        menu = QMenu()
        if self.target == 1:
            CutAction  = menu.addAction("&function 3")
            CutAction2 = menu.addAction("&function 4")
            menu.addAction(CutAction)
            menu.addAction(CutAction2)
            CutAction.triggered.connect(self.function4)
        elif self.target == 2:
            CutAction  = menu.addAction("&function 5")
            CutAction2 = menu.addAction("&function 6")
            menu.addAction(CutAction)
            menu.addAction(CutAction2)
            CutAction.triggered.connect(self.function5)
        menu.exec_(self.tableView.viewport().mapToGlobal(pos))

Another possibility could be to set the attribute for the model, but the concept would be almost the same.

Create different persistent menus, but attached to the models

Similarly to the above, instead of setting a variable (which should be more verbose), you can create the different menus for each model and make them their attributes:

    def func1(self):
        # ...

        menu = QMenu()
        CutAction  = menu.addAction("&function 3")
        CutAction2 = menu.addAction("&function 4")
        menu.addAction(CutAction)
        menu.addAction(CutAction2)
        CutAction.triggered.connect(self.function4)
        self.model.menu = menu

    def onCustomContextMenuRequested(self, pos):
        index = self.tableView.indexAt(pos)
        if index.isValid():
            self.tableView.model().menu.exec_(
                self.tableView.viewport().mapToGlobal(pos))

Note that any implementation above could be a bit more simplicistic (and OOP consistent) if you properly use pyuic files instead of trying to edit them, which is considered bad practice for many reasons, and in this specific case because it often overcomplicates things and could create confusion about the object structure, function purpose, etc. For instance, signal connections should not be done inside retranslateUi, which has a completely different purpose. Read more about using Designer.

User contributions licensed under: CC BY-SA
2 People found this is helpful
Advertisement