c++qmenuqcheckboxqwidgetaction

TriState QAction in QMenu


I'm in need of a checkable QAction, that has besides the modes checked and unchecked the option of a partially check. This is basically already what QCheckBox provides, but unfortunately, QAction doesn't provide.

As a first attempt, I came up with the following approach by implementing a custom QWidgetAction.

TriState.h

#pragma once

#include <QWidgetAction>
#include <QCheckBox>
#include <QLabel>
#include <QFrame>
#include <QHBoxLayout>

class TriStateAction : public QWidgetAction {
    Q_OBJECT
public:
    TriStateAction(QWidget* parent=nullptr) : QWidgetAction(parent) {
        mChkBox = new QCheckBox;
        mChkBox->setTristate(true);
        auto widget = new QFrame;
        widget->setLayout(new QHBoxLayout);
        widget->layout()->addWidget(mChkBox);
        widget->layout()->addWidget(new QLabel("TriState"));

        setDefaultWidget(widget);
        connect(mChkBox, &QCheckBox::stateChanged, this, &QWidgetAction::changed);
    }

    void setCheckState(Qt::CheckState checkState) {
        mChkBox->setCheckState(checkState);
    }
    Qt::CheckState checkState() const {
        return mChkBox->checkState();
    }


private:
    QCheckBox* mChkBox{ nullptr };

};

With this a simple TestRunner:

main.cpp

#include <QApplication>
#include <QMenu>
#include <QAction>
#include "TriStateAction.h"

int main(int argc, char** args) {
    QApplication app(argc, args);
    auto label=new QLabel("Test");
    label->setContextMenuPolicy(Qt::ContextMenuPolicy::CustomContextMenu);
    label->connect(label, &QLabel::customContextMenuRequested, [&](const QPoint& point) {
        QMenu menu(label);
        auto globalPoint = label->mapToGlobal(point);
        auto triStateAction = new TriStateAction();
        auto normalAction = new QAction("Check");
        normalAction->setCheckable(true);
        normalAction->setChecked(true);
        menu.addAction(triStateAction);
        menu.addAction(normalAction);
        menu.exec(globalPoint);
    });

    label->show();
    app.exec();
}

Now, the context menu pops up and I can happily check, uncheck and partially check my TriState Action. But, unlike the ordinary QAction, the TriState will not close the menu upon interaction. How can this do?

Another issue is the different layout (visual representation) of my TriState Action. How can it be made more similar in comparison to the ordinary QAction? (Actually, this seems to be a very hard question.)


Solution

  • Let the action know its menu, adding this line in your main:

    triStateAction->setMenu(&menu);
    

    In TriStateAction class, add a slot to catch the check box stateChanged signal, and close the menu from there:

    private slots:
        void checkBoxStateChanged(int)
        {
            if (menu() != nullptr)
            {
                menu()->close();
            }
        }
    

    Don't forget to connect the slot, in TriStateAction constructor:

    connect(mChkBox, &QCheckBox::stateChanged, this, &TriStateAction::checkBoxStateChanged);