zoukankan      html  css  js  c++  java
  • 处理QMenu的triggered信号时遇到的一个问题

    最近,在一个Qt程序中使用QMenu类时,遇到了一个小问题,特记录下。
    首先,我模仿一下问题出现的场景:
    假设我在做一个高大上的XX管理系统,比如说:学生信息管理系统。在这个系统中,学生的各项信息(比如:姓名、性别、年龄、班级、总分)使用数据库来存储。为了便于老师操作学生数据记录(比如:添加、修改、删除),我使用了一个QTableWidget(嗯,如果在MFC中的话,我会使用CListCtrl/CMFCListCtrl)来显示数据库中的所有学生记录。这个QTableWidget有多列,每列对应数据库中的一项(列)信息。
    现在,我想给这个QTableWidget的header view上添加一个右键快捷菜单,也就是所谓的:Context menu。通过这个Context Menu,我们可以选择让QTableWidget中哪些列显示出来,哪些列不显示。
    类似上面的这种需求很普遍。比如,Win 7系统的资源管理器就提供了这种功能,一图以蔽之:

    怎样在Qt中为一个窗体部件上实现context menu?我找到了一些资料:

    http://www.cnblogs.com/stevenpan/archive/2013/05/29/3105419.html
    http://www.stackoverflow.com/questions/9187538/qt-how-to-add-a-list-of-qactions-to-qmenu-and-handle-them-with-a-single-slot
    http://www.setnode.com/blog/right-click-context-menus-with-qt/

    http://wenku.baidu.com/view/ea3cec4e90c69ec3d5bb75c9.html

    我的测试代码:
    主函数:

    // main.cpp
    #include "mainwindow.h"
    #include <QApplication>
    
    int main(int argc, char *argv[])
    {
        QApplication a(argc, argv);
        MainWindow w;
        w.show();
    
        return a.exec();
    }

    MainWindow的实现:
    头文件:

    // mainwindow.h
    #ifndef MAINWINDOW_H
    #define MAINWINDOW_H
    
    #include <QtWidgets/QMainWindow>
    
    class QAction;
    class QMenu;
    class QTableWidget;
    
    class MainWindow : public QMainWindow
    {
        Q_OBJECT
    
    public:
        MainWindow(QWidget *parent = 0);
        ~MainWindow();
    
    private slots:
        void onShowOrHideColumn(QAction *action);
    
    private:
        QTableWidget *stuInfoWidget;
    
        QMenu *mainMenu;
    };
    
    #endif // MAINWINDOW_H

    实现文件:

    // mainwindow.cpp
    #include "mainwindow.h"
    
    #include <QtCore/QStringList>
    #include <QtWidgets/QAction>
    #include <QtWidgets/QHeaderView>
    #include <QtWidgets/QMenu>
    #include <QtWidgets/QMenuBar>
    #include <QtWidgets/QTableWidget>
    #include <QtWidgets/QTableWidgetItem>
    
    MainWindow::MainWindow(QWidget *parent)
        : QMainWindow(parent)
    {
        QStringList columnNames;
        columnNames << tr("Name") << tr("Sex") << tr("Age");
    
        // 创建一个QTableWidget,共三列,分别显示:Name(姓名)、Sex(性别)、Age(年龄)
        stuInfoWidget = new QTableWidget;
        stuInfoWidget->setColumnCount(columnNames.size());
        for (int i = 0; i < columnNames.size(); ++i) {
            QTableWidgetItem *headerItem = new QTableWidgetItem(columnNames[i]);
            stuInfoWidget->setHorizontalHeaderItem(i, headerItem);
        }
    
        // 创建一个菜单。通过这个菜单,可以选择显示/隐藏指定列。
        mainMenu = menuBar()->addMenu(tr("Show or hide columns"));
        for (int i = 0; i < columnNames.size(); ++i) {
            QAction *action = new QAction(columnNames[i], this);
            // 设定菜单项是可勾选的。
            action->setCheckable(true);
            // 设定菜单项初始状态是已被勾选的。
            action->setChecked(true);
            // 将列序号设定为菜单项的data,这样在槽函数onShowOrHideColumn中,
            // 可以通过调用QAction::data方法来获知要显示/隐藏的列的序号。
            action->setData(i);
            mainMenu->addAction(action);
        }
        // 将mainMenu的triggered(QAction *)信号连接到自定义槽函数
        // onShowOrHideColumn(QAction *action)上。这样,当用户触发mainMenu
        // 中某一菜单项时,onShowOrHideColumn(QAction *)会被自动调用。
        // QAction *类型的参数action指向被触发的菜单项。
        connect(mainMenu, SIGNAL(triggered(QAction *)),
                this, SLOT(onShowOrHideColumn(QAction *)));
    
        // 给QTableWidget的header view添加context menu的一种方法。
        QHeaderView *headerView = stuInfoWidget->horizontalHeader();
        headerView->setContextMenuPolicy(Qt::ActionsContextMenu);
        headerView->addActions(mainMenu->actions());
    
        setCentralWidget(stuInfoWidget);
    }
    
    MainWindow::~MainWindow()
    {
    }
    
    void MainWindow::onShowOrHideColumn(QAction *action)
    {
        // 稍后会添加该函数的实现代码。
    }

    Qt通过“信号-槽”机制来处理消息。“信号”可以视为Windows中的“消息”,而“槽”则可拿MFC/SDK中的消息映射函数/消息回调函数来类比。当然,Qt中的“信号-槽”机制不仅仅局限于可以接收消息的窗体部件。如果想深入了解Qt的“信号-槽”机制的实现方式,可以看看这篇博文:
    http://www.woboq.com/blog/how-qt-signals-slots-work.html

    接下来,该让onShowOrHideColumn做些什么了。起初,我的想法是,在槽函数中,通过action的isChecked方法来判断这个action是否已经checked。如果是,那么显示这个action所管理的列,然后调用action->setChecked(false)来取消这个action的checked状态;如果不是,则隐藏相应列,并setChecked(true)。具体代码如下:

    void MainWindow::onShowOrHideColumn(QAction *action)
    {
        // 获取当前的checked状态。
        bool isChecked = action->isChecked();
        // 在构造函数中,我们创建QAction对象的时候,通过setData把
        // 这个QAction的user data设置为其管理的列的序号。
        // 这里,通过data方法取出这个列编号,然后调用setColumnHidden来显示/隐藏该列。
        stuInfoWidget->setColumnHidden(action->data().toInt(),
                                       isChecked);
        // 设置新的checked状态。
        action->setChecked(!isChecked);
    }

    乍一看,这段代码内容充实,主题明确,非常感人。但是,它无法满足我们所需的效果。如果您实践一下的话,会发现菜单项的checked属性始终是true,而且不管您怎么点击菜单项,都无法更改QTableWidget某一列的显示/隐藏状态。

    由于在Qt文档中以及Google上都没找到有用的线索,所以我在QAction这个类的源程序文件中凡是涉及修改QAction的checked属性的代码行上都加了断点,然后进行调试跟踪(实在是一种笨方法,不过好在QAction的代码执行逻辑并不那么“晦涩”)。我发现,在一个action被触发后,QAction::activate方法会先被调用,然后才是onShowOrHideColumn这个槽函数。下面是一段摘自QAction源文件中的代码:

    void QAction::activate(ActionEvent event)
    {
        Q_D(QAction);
        if(event == Trigger) {
            QPointer<QObject> guard = this;
            if(d->checkable) {
                // the checked action of an exclusive group cannot be  unchecked
                if (d->checked && (d->group && d->group->isExclusive()
                                   && d->group->checkedAction() == this)) {
                    if (!guard.isNull())
                        emit triggered(true);
                    return;
                }
                setChecked(!d->checked);
            }
            if (!guard.isNull())
                emit triggered(d->checked);
        } else if(event == Hover) {
            emit hovered();
        }
    }

    注意该方法中的这一句代码:

    setChecked(!d->checked);

    可见,一个checkable的QAction对象被触发后,其checked状态会在QAction::activate被更新。所以,我们在QMenu::triggered(QAction *action)对应的槽函数中通过action调用isChecked将得到更新后的checked状态,而非当前的状态。

    如果在Qt文档中仔细查找的话,还是可以得出这样的结论的:

    void QAction::activate(ActionEvent event)
    Sends the relevant signals for ActionEvent event.
    Action based widgets use this API to cause the QAction to emit signals as well as emitting their own.

    enum QAction::ActionEvent
    This enum type is used when calling QAction::activate()
    QAction::Trigger
    this will cause the QAction::triggered() signal to be emitted.

    void QAction::toggle() [slot]
    This is a convenience function for the checked property. Connect to it to change the checked state to its opposite state.

    了解了这些后,重写这个槽函数为:

    void MainWindow::onShowOrHideColumn(QAction *action)
    {
        // 对于一个checkable的QAction对象,如果最初其checked属性:
        // 1) == true, 那么在其被触发后,其checked属性将会在
        //    QAction::activate方法中被更改为false。因此,随后我们
        //    在这个槽函数中调用isChecked时将得到false。
        // 2) 和情况1)相反。
        // 而且,我们无需自己调用setChecked方法来更新checked状态,因为
        // QAction::activate已经帮我们做了这步工作。
        stuInfoWidget->setColumnHidden(action->data().toInt(),
                                       !action->isChecked());
    }

    Qt的这种做法的确让我们少写了几行代码,不过如果不知道这一点的话,会让如我这样的新手感到困惑的。
    本文的示例程序:

    https://files.cnblogs.com/myd7349/StudentInfo_v1.zip

    PS1:在使用Windows SDK编写Win32 GUI程序的时候,我们往往需要使用诸如CheckMenuItem、ModifyMenu、SetMenuItemInfo等这样的API来显式check或uncheck一个菜单项。
    PS2:在MFC中,要更改一个菜单项的checked状态,需要为菜单项添加ON_UPDATE_COMMAND_UI消息映射,然后在消息映射函数中通过CCmdUI *类型的参数的setCheck方法来实现。
    PS3:正是因为我在MFC & SDK中的编程经验,才使我写出了上面那个错误版本的槽函数。

    PS4:第一次使用博客园的博客,还不会排版。见谅啊。^_^

  • 相关阅读:
    第十二章学习笔记
    UVa OJ 107 The Cat in the Hat (戴帽子的猫)
    UVa OJ 123 Searching Quickly (快速查找)
    UVa OJ 119 Greedy Gift Givers (贪婪的送礼者)
    UVa OJ 113 Power of Cryptography (密文的乘方)
    UVa OJ 112 Tree Summing (树的求和)
    UVa OJ 641 Do the Untwist (解密工作)
    UVa OJ 105 The Skyline Problem (地平线问题)
    UVa OJ 100 The 3n + 1 problem (3n + 1问题)
    UVa OJ 121 Pipe Fitters (装管子)
  • 原文地址:https://www.cnblogs.com/myd7349/p/QMenu_triggered_signal.html
Copyright © 2011-2022 走看看