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

Posted

技术标签:

【中文标题】防止 QMenu 在其 QAction 之一被触发时关闭【英文标题】:Prevent a QMenu from closing when one of its QAction is triggered 【发布时间】:2010-01-12 16:27:53 【问题描述】:

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

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

有什么想法吗?谢谢。

【问题讨论】:

【参考方案1】:

将 QWidgetAction 和 QCheckBox 用于不会导致菜单关闭的“可检查操作”。

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

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

【讨论】:

非常感谢。对于塑料风格,确实有一个余量可以添加。所以我把复选框放在一个带有布局的小部件中,并设置它的边距(也许有更简单的方法......)最后一件事:复选框不会扩展到菜单的整个宽度,所以如果点击发生在之后框标签的末尾关闭菜单并且不选中框。设置尺寸政策无效。 这不适用于 Ubuntu Unity 上的 QsystemTrayIcon.contextMenu(),因为 Unity 不会从 QWidgetAction 内部显示小部件 @gregseth 有没有办法将复选框扩展到菜单的整个宽度?【参考方案2】:

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

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

这是一个重新实现 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() 中的复杂算法。

【讨论】:

这与我刚刚提出的解决方案完全相同,据我所知,它运行良好。我想我应该在自己尝试之前阅读这里的所有答案。 您也可以不调用QMenu::mouseReleaseEvent,而不是禁用和启用该操作。在这种情况下,它就像一个魅力。覆盖 keyPressEvent 并为空格按钮添加行为也可以正常工作。 这种方法的问题是当菜单不适合屏幕时,菜单的顶部/底部会有箭头向上/向下滚动。触发操作后,菜单会显示其总高度(没有滚动条),因此您无法看到整个菜单。【参考方案3】:

这是我的解决方案:

    // 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);

【讨论】:

【参考方案4】:

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

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

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

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

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

希望这会有所帮助!

【讨论】:

【参考方案5】:

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

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

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

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

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

在一天结束时(字面意思),我已经让整个菜单做对了。我只是想添加这个功能,我绝对不想改变为为此使用小部件。出于这个原因,我暂时保留了这个肮脏的补丁。

【讨论】:

【参考方案6】:

从baysmith解决方案开始,复选框没有按我的预期工作,因为我连接到动作触发(),而不是连接到复选框切换(布尔)。当我按下按钮时,我正在使用代码打开一个带有多个复选框的菜单:

           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));

对于我的用例,这就像一个魅力

【讨论】:

【参考方案7】:

子类 QMenu 并覆盖 setVisible。您可以利用 activeAction() 来了解是否选择了某个操作,并使用可见 arg 来查看 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);
    
;

【讨论】:

以上是关于防止 QMenu 在其 QAction 之一被触发时关闭的主要内容,如果未能解决你的问题,请参考以下文章

如何知道触发了哪个 QMenu 的动作

Qt QAction 动态数组连接

QMenu - 快捷方式未触发

Qt Connect 触发 QMenu 的 SIGNAL

如果 QMenu 是 unique_ptr,为啥 QAction 不添加到 QMenu?

如何将 QMenu 中的 QAction 转换为 QWidget?