QAction toggling is hinder by QMenu mousePressEven

2019-08-17 18:07发布

问题:

I have a nested menu items in which I am trying to make all of the items checkable. Initially the toggling is not working for the main item (a QMenu that is set as a QAction), while it works for sub items.

But upon the use of mousePressEvent, the toggling now works for the main item but not for the sub items.

Tried to replicate what I have done for main item for the sub item function, but the toggling still does not work. And seemingly, it is called twice in the _callActionItem().

However, for some reasons, if the sub-item window is in tear off mode, toggling is possible but not so, if you do a right click menu in the tool itself.

Additionally, if I disable mousePressEvent in QCustomMenu, I am back to square one where toggling works for sub-items but not the main-items

class QSubAction(QtGui.QAction):
    def __init__(self, text="", parent=None):
        super(QSubAction, self).__init__(text, parent)
        self.setCheckable(True)
        self.setChecked(True)


class QAddAction(QtGui.QAction):
    def __init__(self, icon=None, text="Add Item", parent=None):
        if icon:
            super(QAddAction, self).__init__(icon, text, parent)
        else:
            super(QAddAction, self).__init__(text, parent)

class QCustomMenu(QtGui.QMenu):
    """Customized QMenu."""

    def __init__(self, title, parent=None):
        super(QCustomMenu, self).__init__(title=str(title), parent=parent)
        self.setup_menu()

    def mousePressEvent(self, event):
        action = self.activeAction()
        if isinstance(action, QtGui.QAction):
            action.trigger()
        return QtGui.QMenu.mousePressEvent(self, event)

    def setup_menu(self):
        self.setContextMenuPolicy(QtCore.Qt.DefaultContextMenu)

    def contextMenuEvent(self, event):
        no_right_click = [QAddAction]
        if any([isinstance(self.actionAt(event.pos()), instance) for instance in no_right_click]):
            return
        pos = event.pos()

    def addAction(self, action):
        super(QCustomMenu, self).addAction(action)


class Example(QtGui.QMainWindow):
    def __init__(self, parent=None):
        super(Example, self).__init__(parent)
        self.initUI()

    def initUI(self):         
        self.setGeometry(300, 300, 300, 200)
        self.setWindowTitle('Context menu')   

        self.qmenu = QCustomMenu(title='', parent=self)
        add_item_action = QtGui.QAction('Add Main item', self,
            triggered=self.add_new_item)
        self.qmenu.addAction(add_item_action)

    def contextMenuEvent(self, event):
        action = self.qmenu.exec_(self.mapToGlobal(event.pos()))

    def add_new_item(self):
        main_menu_name, ok = QtGui.QInputDialog.getText(
            self,
            'Main Menu',
            'Name of new Menu Item:'
        )
        if ok:
            self._addMenuItemTest(main_menu_name)

    def _addMenuItemTest(self, main_menu_name):
        icon_path = '/user_data/add.png'

        base_qmenu = QCustomMenu(title=main_menu_name, parent=self)
        base_qmenu.setTearOffEnabled(True)                     

        add_item_action = QAddAction(None, 'Add Sub Item', base_qmenu)
        slot = functools.partial(self.add_sub_item, base_qmenu)
        add_item_action.triggered.connect(slot)
        base_qmenu.addAction(add_item_action)

        test_action = QtGui.QAction(main_menu_name, self)
        test_action.setMenu(base_qmenu)
        test_action.setCheckable(True)
        test_action.setChecked(True)

        self.connect(
            test_action,
            QtCore.SIGNAL("triggered(bool)"),
            self.main_toggling
        )

        self.qmenu.addAction(test_action)

    def main_toggling(self, check_state):
        sender_obj = self.sender()
        if isinstance(sender_obj, QtGui.QAction):
            sender_obj.setChecked(check_state)

    def add_sub_item(self, base_menu):
        sub_menu_name, ok = QtGui.QInputDialog.getText(
            self,
            'Sub Menu',
            'Name of new Sub Item:'
        )
        if ok:
            action = QSubAction(sub_menu_name, self)
            slot = functools.partial(
                self._callActionItem,
                action
            )
            # action.toggled.connect(slot)
            # from pprint import pprint
            # pprint(help(action))
            # action.connect(action, QtCore.SIGNAL("triggered(bool)"), self._callActionItem)

            base_menu.addAction(action)

    def _callActionItem(self, action):
        # This is called twice, False and True again
        print '>>> sub check-state : ', action.isChecked()


if __name__ == "__main__":
    app = QtGui.QApplication(sys.argv)
    window = Example()
    window.show()
    sys.exit(app.exec_())

回答1:

By default QMenu will only make the QAction without children can check, so you have done a trick to enable the functionality of another QAction that does not meet the above that may affect the operation, and that is what happens in your case. The solution is to discriminate which type of QAction is pressed:

def mousePressEvent(self, event):
    action = self.activeAction()
    if not isinstance(action, QSubAction) and action is not None:
        action.trigger()
        return
    return QtGui.QMenu.mousePressEvent(self, event)


回答2:

QtGui.QAction.toggle() This is a convenience function for the checked() property. Connect to it to change the checked state to its opposite state.

and

If I execute your code, TypeError occured. (After adding the submenu and click the submenu action items)

TypeError: _callActionItem() takes 3 positional arguments but 4 were given

It seems that you prepare affluent arguments for _callActionItem().

But it is a pitfall.

Because toggle demands an argument for bool.

So, you must change the arguments for _callActionItem() to _callActionItem(self,checked,action)

and toggled is after toggling.

And toggle is called by QSubAction.

You change mousePressEvent,for example, like this.

1.

def mousePressEvent(self,event):
    action = self.activeAction()
    if not isinstance(action,QSubAction) and action is not None:
        action.trigger()
        return
    elif isinstance(action,QSubAction):
        action.toggle()
        return
    return QtGui.QMenu.mousePressEvent(self,event)

and

2.

_callActionItem(self,checked,action)

and the all changed code here.


import functools
import sys
from PyQt4 import QtGui, QtCore


class QSubAction(QtGui.QAction):
    def __init__(self, text="", parent=None):
        super(QSubAction, self).__init__(text, parent)
        self.setCheckable(True)
        self.setChecked(True)

class QAddAction(QtGui.QAction):
    def __init__(self, icon=None, text="Add Item", parent=None):
        if icon:
            super(QAddAction, self).__init__(icon, text, parent)
        else:
            super(QAddAction, self).__init__(text, parent)

class QCustomMenu(QtGui.QMenu):
    """Customized QMenu."""

    def __init__(self, title, parent=None):
        super(QCustomMenu, self).__init__(title=str(title), parent=parent)
        self.setup_menu()
    def mousePressEvent(self,event):
        action = self.activeAction()
        if not isinstance(action,QSubAction) and action is not None:
            action.trigger()
            return
        elif isinstance(action,QSubAction):
            action.toggle()
            return
        return QtGui.QMenu.mousePressEvent(self,event)
    def setup_menu(self):
        self.setContextMenuPolicy(QtCore.Qt.DefaultContextMenu)

    def contextMenuEvent(self, event):
        no_right_click = [QAddAction]
        if any([isinstance(self.actionAt(event.pos()), instance) for instance in no_right_click]):
            return
        pos = event.pos()

    def addAction(self, action):
        super(QCustomMenu, self).addAction(action)

class Example(QtGui.QMainWindow):
    def __init__(self, parent=None):
        super(Example, self).__init__(parent)
        self.initUI()

    def initUI(self):         
        self.setGeometry(300, 300, 300, 200)
        self.setWindowTitle('Context menu')    

        self.qmenu = QCustomMenu(title='', parent=self)
        add_item_action = QtGui.QAction('Add Main item', self,
            triggered=self.add_new_item)
        self.qmenu.addAction(add_item_action)

    def contextMenuEvent(self, event):
        action = self.qmenu.exec_(self.mapToGlobal(event.pos()))

    def add_new_item(self):
        main_menu_name, ok = QtGui.QInputDialog.getText(
            self,
            'Main Menu',
            'Name of new Menu Item:'
        )
        if ok:
            self._addMenuItemTest(main_menu_name)

    def _addMenuItemTest(self, main_menu_name):
        icon_path = '/user_data/add.png'

        base_qmenu = QCustomMenu(title=main_menu_name, parent=self)
        base_qmenu.setTearOffEnabled(True)                     

        add_item_action = QAddAction(None, 'Add Sub Item', base_qmenu)
        slot = functools.partial(self.add_sub_item, base_qmenu)
        add_item_action.triggered.connect(slot)
        base_qmenu.addAction(add_item_action)
        # self.qmenu.addMenu(base_qmenu)

        test_action = QtGui.QAction(main_menu_name, self)
        test_action.setMenu(base_qmenu)
        test_action.setCheckable(True)
        test_action.setChecked(True)
        self.connect(test_action,QtCore.SIGNAL("triggered(bool)"),self.unsetCheck_action)


        self.qmenu.addAction(test_action)

    def unsetCheck_action(self,checked):

        sender_obj = self.sender()
        if isinstance(sender_obj,QtGui.QAction): 


            sender_obj.setChecked(checked)




    def add_sub_item(self, base_menu):
        sub_menu_name, ok = QtGui.QInputDialog.getText(
            self,
            'Sub Menu',
            'Name of new Sub Item:'
        )
        if ok:

            action = QSubAction(sub_menu_name, self)
            slot = functools.partial(
                self._callActionItem,
                action
            )
            action.setCheckable(True)
            action.setChecked(True)
            action.toggled.connect(slot)
            base_menu.addAction(action)

    def _callActionItem(self, checked, action):
        # This is called twice, False and True again
        print '>>> sub check-state : ', action.isChecked()



if __name__ == "__main__":
    app = QtGui.QApplication(sys.argv)
    window = Example()
    window.show()
    sys.exit(app.exec_())