處理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的這種做法的確讓我們少寫了幾行代碼,不過如果不知道這一點的話,會讓如我這樣的新手感到困惑的。
本文的示例程序:

http://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:第一次使用博客園的博客,還不會排版。見諒啊。^_^


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM