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:第一次使用博客园的博客,还不会排版。见谅啊。^_^

  • 相关阅读:
    WP8.1 UI 编程 四、图形
    分治法 全排列问题的一个Java实现
    合并排序算法时间复杂度分析
    js如何实现复制粘贴功能
    关于<meta name="applicable-device"content="pc">
    ready
    css文字强制大写输入
    关于input,button标签在苹果手机上显示阴影解决办法
    【C#】中用if判断值是否是否为空
    视频格式MP4,需要转码
  • 原文地址:https://www.cnblogs.com/myd7349/p/QMenu_triggered_signal.html
Copyright © 2011-2022 走看看