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(watched))
{
if (actionItem->rect().contains(actionItem->mapFromGlobal(QCursor::pos())))
{
actionItem->SetBackgroundRole(true);
}
else
{
actionItem->SetBackgroundRole(false);
}
}
}
return QSystemTrayIcon::eventFilter(watched, event);
}