防止 QMenu 在其 QAction 之一被触发时关闭

问题描述 投票:0回答:10

我使用 QMenu 作为上下文菜单。该菜单充满了 QActions。其中一个 QAction 是可检查的,我希望能够在不关闭上下文菜单的情况下检查/取消检查它(并且必须再次重新打开它来选择我想要的选项)。

我尝试断开可检查的 QAction 发出的信号,但没有成功。

有什么想法吗?谢谢。

c++ qt qt4
10个回答
26
投票

使用 QWidgetAction 和 QCheckBox 进行“可检查操作”,这不会导致菜单关闭。

QCheckBox *checkBox = new QCheckBox(menu);
QWidgetAction *checkableAction = new QWidgetAction(menu);
checkableAction->setDefaultWidget(checkBox);
menu->addAction(checkableAction);

在某些样式中,这不会与可检查操作完全相同。例如,对于 Plastique 样式,复选框需要缩进一点。


12
投票

似乎没有任何优雅的方法来阻止菜单关闭。但是,只有当操作实际上可以触发(即已启用)时,菜单才会关闭。因此,我发现的最优雅的解决方案是通过在触发操作时短暂禁用该操作来欺骗菜单。

  1. QMenu 子类
  2. 重新实现相关事件处理程序(如 mouseReleaseEvent())
  3. 在事件处理程序中,禁用该操作,然后调用基类的实现,然后再次启用该操作,并手动触发它

这是重新实现 mouseReleaseEvent() 的示例:

void mouseReleaseEvent(QMouseEvent *e)
{
    QAction *action = activeAction();
    if (action && action->isEnabled()) {
        action->setEnabled(false);
        QMenu::mouseReleaseEvent(e);
        action->setEnabled(true);
        action->trigger();
    }
    else
        QMenu::mouseReleaseEvent(e);
}

为了使解决方案完美,应该在所有可能触发该操作的事件处理程序中执行类似的操作,例如 keyPressEvent() 等...

问题在于,要知道您的重新实现是否应该真正触发该操作,甚至应该触发哪个操作,并不总是那么容易。最困难的可能是通过助记符触发动作:您需要自己重新实现 QMenu::keyPressEvent() 中的复杂算法。


2
投票

这是我的解决方案:

    // this menu don't hide, if action in actions_with_showed_menu is chosen.
    class showed_menu : public QMenu
    {
      Q_OBJECT
    public:
      showed_menu (QWidget *parent = 0) : QMenu (parent) { is_ignore_hide = false; }
      showed_menu (const QString &title, QWidget *parent = 0) : QMenu (title, parent) { is_ignore_hide = false; }
      void add_action_with_showed_menu (const QAction *action) { actions_with_showed_menu.insert (action); }

      virtual void setVisible (bool visible)
      {
        if (is_ignore_hide)
          {
            is_ignore_hide = false;
            return;
          }
        QMenu::setVisible (visible);
      }

      virtual void mouseReleaseEvent (QMouseEvent *e)
      {
        const QAction *action = actionAt (e->pos ());
        if (action)
          if (actions_with_showed_menu.contains (action))
            is_ignore_hide = true;
        QMenu::mouseReleaseEvent (e);
      }
    private:
      // clicking on this actions don't close menu 
      QSet <const QAction *> actions_with_showed_menu;
      bool is_ignore_hide;
    };

    showed_menu *menu = new showed_menu ();
    QAction *action = menu->addAction (new QAction (menu));
    menu->add_action_with_showed_menu (action);

1
投票

这是我的一些想法...但完全不确定它们是否会起作用;)

1) 尝试使用 QMenu 的 aboutToHide() 方法捕获事件;也许你可以“取消”隐藏过程?

2)也许你可以考虑使用 EventFilter ?

尝试看看:http://qt.nokia.com/doc/4.6/qobject.html#installEventFilter

3)否则你可以重新实现 QMenu 以添加你自己的行为,但这对我来说似乎有很多工作......

希望这对您有一点帮助!


1
投票

(我从Andy的回答开始,所以谢谢Andy!)

1) aboutToHide() 可以通过在缓存位置重新弹出菜单来工作,但它也可以进入无限循环。测试是否在菜单外单击鼠标以忽略重新打开应该可以解决问题。

2)我尝试了事件过滤器,但它阻止了对菜单项的实际单击。

3) 两者都用。

这里有一个肮脏的模式来证明它是有效的。当用户在单击时按住 CTRL 键时,这会使菜单保持打开状态:

    # in __init__ ...
    self.options_button.installEventFilter(self)
    self.options_menu.installEventFilter(self)
    self.options_menu.aboutToHide.connect(self.onAboutToHideOptionsMenu)

    self.__options_menu_pos_cache = None
    self.__options_menu_open = False

def onAboutToHideOptionsMenu(self):
    if self.__options_menu_open:          # Option + avoid an infinite loop
        self.__options_menu_open = False  # Turn it off to "reset"
        self.options_menu.popup(self.__options_menu_pos_cache)

def eventFilter(self, obj, event):
    if event.type() == QtCore.QEvent.MouseButtonRelease:
        if obj is self.options_menu:
            if event.modifiers() == QtCore.Qt.ControlModifier:
                self.__options_menu_open = True

            return False

        self.__options_menu_pos_cache = event.globalPos()
        self.options_menu.popup(event.globalPos())
        return True

    return False

我说它很脏,因为这里的小部件充当打开菜单的按钮以及菜单本身的事件过滤器。使用显式事件过滤器类很容易添加,并且会让事情变得更容易理解。

布尔值可能会被替换为检查鼠标是否位于菜单上,如果没有,请不要将其打开。但是,对于我的用例,仍然需要考虑 CTRL 键,因此它可能离一个好的解决方案不远了。

当用户按住 CTRL 并单击菜单时,它会翻转一个开关,以便菜单在尝试关闭时自行重新打开。该位置被缓存,因此它会在同一位置打开。有一个快速闪烁,但感觉还不错,因为用户知道他们按住一个键来完成这项工作。

一天结束时(字面意思)我已经让整个菜单做了正确的事情。我只是想添加此功能,而且我绝对不想为此而更改为使用小部件。因此,我现在甚至保留了这个脏补丁。


1
投票

将 QMenu.show 连接到操作触发器。我知道这是 Qt5 的代码(和 Python),但原理应该是向后兼容的。

from PyQt5 import QtWidgets

class CheckableMenu(QtWidgets.QMenuBar):
    
    def __init__(self,parent=None):
        super().__init__(parent)
        
        self.menuObj=QtWidgets.QMenu("View")
        self.addMenu(self.menuObj)
        for i in ['Both','Even','Odd']: #my checkable items
            t=QtWidgets.QAction(i,self.menuObj,checkable=True)
            t.triggered.connect(self.menuObj.show)
            self.menuObj.addAction(t)

1
投票

我已经为此苦恼了半天了。

网上有很多接受的答案建议重写 QMenu 的 setVisible 函数,但这对我来说根本不起作用。 我根据这篇文章找到了一个解决方案(OP的最后回复)

我对此事的C++实现如下:

bool MainWindow::eventFilter(QObject *watched, QEvent *event)
{
    if (event->type() == QEvent::MouseButtonRelease)
    {
        auto action = static_cast<QMenu*>(watched)->activeAction();
        if (action && action->isCheckable())
        {
            action->trigger();
            return true;
        }
    }
    return QObject::eventFilter(watched, event);
}

0
投票

子类化 QMenu 并重写 setVisible。您可以利用 activeAction() 来了解是否选择了某个操作,并使用可见参数来查看 QMenu 是否正在尝试关闭,然后您可以使用您想要的值覆盖并调用 QMenu::setVisible(...) 。

class ComponentMenu : public QMenu
{
public:
    using QMenu::QMenu;

    void setVisible(bool visible) override
    {
        // Don't hide the menu when holding Shift down
        if (!visible && activeAction())
            if (QApplication::queryKeyboardModifiers().testFlag(Qt::ShiftModifier))
                return;

        QMenu::setVisible(visible);
    }
};

0
投票

可以这样做:

menuSettings = menubar.addMenu('&Settings')
selection_debug = QAction('Debug', self, checkable=True)
selection_debug.triggered.connect(lambda : menuSettings.show())

menuSettings.addAction(selection_debug)

0
投票

使用这个答案,复选框没有像我预期的那样工作,因为我连接到插槽

triggered()
,而不是连接到复选框的
toggled(bool)

当我按下按钮时,我使用下面的代码打开一个带有多个复选框的菜单:

QMenu menu;

QCheckBox *checkBox = new QCheckBox("Show Grass", &menu);
checkBox->setChecked(m_showGrass);

QWidgetAction *action = new QWidgetAction(&menu);
action->setDefaultWidget(checkBox);

menu.addAction(action);
//connect(action, SIGNAL(triggered()), this, SLOT(ToggleShowHardscape_Grass()));
connect(checkBox, SIGNAL(toggled(bool)), this, SLOT(ToggleShowHardscape_Grass()));

menu.exec(QCursor::pos() + QPoint(-300, 20));
© www.soinside.com 2019 - 2024. All rights reserved.