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