qt實現菜單,簡單的界面QMenu+QAction完全可以實現,在加上qss的支持,可以定制出比較美觀的菜單,qt的菜單一般用在托盤、按鈕和工具欄上。
當然啦,也有很多軟件有比較美觀的托盤菜單,比如360、電腦管家等軟件,效果圖如圖1所示,其實qt在4.2之后也提供了定制菜單的功能,使用QWidgetAction可以定制出自己想要的菜單來,接下來是我定制菜單欄的步驟。
圖1 360圖盤菜單
實現效果如下圖2所示,菜單是由單個條目組成的,每一個條目又由左右兩部分組成,左邊是一個圖標,並伴有底色,右邊是一個label,上邊有文字描述,當有鼠標移動到項上時,項整個背景色變成紅色,並且圖標會替換,文字顏色也會有相應的變化。
圖2 定制菜單
首先拿到這個功能,我們可以先考慮功能的拆分,既然qt支持菜單項的窗口定制功能,那我們不防把每一個項目定制成一個QWidget,這樣就問題就變成了一個窗口的定制,這樣看起來是不是簡單多了。
首先我們來看下QSystemTrayIcon類,該類實現了windows托盤的功能,activated信號表示托盤圖標有事件,我們需要處理這個信號,當messageClicked信號觸發時,說明我們點擊了托盤提示信息。下面是我重寫的托盤類

1 class CSystemTrayIcon : public QSystemTrayIcon 2 { 3 Q_OBJECT 4 5 signals: 6 void ShowMainWidget(); 7 void ShowMiniWidget(); 8 void AppQuit(); 9 10 public: 11 CSystemTrayIcon(const QIcon & icon, QObject * parent = nullptr); 12 ~CSystemTrayIcon(); 13 14 public: 15 void SetWaverable(bool waver);//托盤圖標是否閃動 16 void ShowMessage(const QString & title 17 , const QString & message 18 , QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::Information 19 , int millisecondsTimeoutHint = 5000);//托盤彈出氣泡提示 20 21 QAction * AddAction(const QString & actName, const QIcon & icon); 22 23 protected: 24 virtual bool event(QEvent *) Q_DECL_OVERRIDE; 25 virtual void timerEvent(QTimerEvent *) Q_DECL_OVERRIDE; 26 virtual bool eventFilter(QObject * watched, QEvent * event) Q_DECL_OVERRIDE; 27 28 private slots: 29 void TrayActivateSlot(QSystemTrayIcon::ActivationReason); 30 void MessageClickedSlot(); 31 32 private: 33 void CreateMenu(); 34 35 private: 36 bool m_MouseLeave = true; 37 int m_TimeID = 0; 38 QIcon m_IconPath; 39 CTrayMenu * m_Menu = nullptr; 40 #ifdef CustomAction 41 //添加定制菜單項 42 CActionItem * mainAct = nullptr; 43 CActionItem * miniAct = nullptr; 44 CActionItem * exitAct = nullptr; 45 #else 46 QAction * mainAct = nullptr; 47 QAction * miniAct = nullptr; 48 QAction * exitAct = nullptr; 49 #endif 50 };
通過查看CSystemTrayIcon類的接口,可以發現該類中有其他的接口,但是我在本文中不打算對其一一作出解釋,因為和菜單項定制無關,如果非要追問,那我只能接單的說明下,SetWaverable接口使用類設置托盤圖標是否閃動,就類似qq一樣的效果。
接下來我們需要了解下QWidgetAction類,這個類繼承自QAction,他擁有QAction的所有信號和槽,因此我們可以把他當QAction一樣的用,不僅僅如此,我們還可以為其提供自定義的QWidget,實現代碼如下

1 class CActionItem : public QWidgetAction 2 { 3 Q_OBJECT 4 Q_PROPERTY(bool m_Hover READ IsMHover WRITE SetMHover) 5 6 public: 7 CActionItem(const QString & text = "", QWidget * parent = nullptr); 8 ~CActionItem(); 9 10 public: 11 void SetContentText(const QString & text); 12 void SetItemIcon(const QString & icon); 13 void SetItemIcon(const QString & icon, const QString & hover); 14 void SetItemIcon(const QString & icon, const QString & hover, const QString & press); 15 16 QWidget * contentWidget() const;//獲取中心窗口 17 void SetToolTip(const QString & toolTip); 18 19 public: 20 bool IsMHover(){ return m_Hover; } 21 void SetMHover(bool hover){ m_Hover = hover; } 22 23 protected: 24 virtual QWidget * createWidget(QWidget * parent) Q_DECL_OVERRIDE; 25 virtual void deleteWidget(QWidget * widget) Q_DECL_OVERRIDE; 26 27 private: 28 bool m_Hover = false; 29 CActionContentWidget * m_ContentWidget = nullptr; 30 };
當有QWidgetAction被創建時,首先在構造函數中初始化我們定制的窗口,並將其設置為缺省的窗口
CActionItem::CActionItem(const QString & text, QWidget * parent /*= nullptr*/) : QWidgetAction(parent) { setEnabled(true); m_ContentWidget = new CActionContentWidget(); connect(m_ContentWidget, &CActionContentWidget::IconClicked, this, [this]{this->triggered(); }); m_ContentWidget->SetContentText(text); setDefaultWidget(m_ContentWidget); }
createWidget接口會被自動調用,因此我們可以在此接口中創建我們自己定制的QWidget。代碼如下,記得把定制的窗口設置為參數所給窗口的子窗口
QWidget * CActionItem::createWidget(QWidget * parent) { m_ContentWidget->setParent(parent); return m_ContentWidget; }
QWidgetActoin只是一個QAction,想要美觀的菜單項,還是需要我們自己去定制窗口的,接下來就是我自己定制的窗口

1 class CActionContentWidget : public QWidget 2 { 3 Q_OBJECT 4 signals: 5 void IconClicked(); 6 7 public: 8 CActionContentWidget(QWidget * parent = nullptr); 9 ~CActionContentWidget(); 10 11 public: 12 void SetContentText(const QString & text); 13 void SetItemIcon(const QString & icon, const QString & hover); 14 15 public: 16 void SetBackgroundRole(bool hover); 17 18 protected: 19 virtual void enterEvent(QEvent *) Q_DECL_OVERRIDE; 20 virtual void leaveEvent(QEvent *) Q_DECL_OVERRIDE; 21 virtual bool eventFilter(QObject *, QEvent *) Q_DECL_OVERRIDE; 22 23 private: 24 void InitializeUI(); 25 26 private: 27 QWidget * m_ContentWidget = nullptr; 28 QPushButton * m_ActIcon = nullptr; 29 QLabel * m_ActText = nullptr; 30 QString m_NormalIcon, m_HoverIcon, m_PressedIcon; 31 };
最后就是菜單的定制啦,為什么要重寫菜單呢,因為我需要在指定時刻,修改菜單項的位置,因此菜單項的定制也比較簡單,就是在關鍵時刻跑出一個信號,表示需要修改菜單位置了,代碼如下:

1 class CTrayMenu : public QMenu 2 { 3 Q_OBJECT 4 5 signals: 6 void FixedPostion();//移動菜單位置 7 8 public: 9 CTrayMenu(QWidget * parent = nullptr); 10 ~CTrayMenu(); 11 12 protected: 13 virtual bool event(QEvent *) Q_DECL_OVERRIDE; 14 };
因為菜單是一個QWidget,在構造函數中,拿不到width和height,而在show的時候可以拿到相關信息,代碼如下:
bool CTrayMenu::event(QEvent * e) { if (e->type() == QEvent::Show) { emit FixedPostion(); } return QMenu::event(e); }
講到這兒,qt菜單定制功能就講完了,在菜單定制的過程中我自己也遇到了一些問題,在此記錄下,希望看到並知道原因的留下您的腳印。
問題:
1、定制的QWidget中的鼠標事件異常
2、qss中的屬性判斷異常,例如QLabel[IsCheck=true]{border:1 solid #ff0000;},這種方式設置的鼠標變化不起作用,為了實現這個功能,我是在CSystemTrayIcon類中把定制窗口事件都注冊到父類中,然后通過eventFilter來判斷鼠標位置,進一步重新設置qss來到達鼠標移動換背景色的功能。代碼如下:
bool CSystemTrayIcon::eventFilter(QObject * watched, QEvent * event) { if (watched == this) { m_MouseLeave = false; } if (watched->inherits("QWidget") && event->type() == QEvent::Paint) { if (CActionContentWidget * actionItem = static_cast<CActionContentWidget *>(watched)) { if (actionItem->rect().contains(actionItem->mapFromGlobal(QCursor::pos()))) { actionItem->SetBackgroundRole(true); } else { actionItem->SetBackgroundRole(false); } } } return QSystemTrayIcon::eventFilter(watched, event); }