需求是滑鼠滑過菜單項時,菜單項的文字、icon以及子菜單的小箭頭都要高亮顯示,qss中隻能設定item背景色、文字顔色以及子菜單小箭頭的樣式,icon的圖檔不能切換,另外曾經想過用indicator(對action setCheckable(true)後,此子控件在qss中會生效)代替icon,因為indicator可以在qss中定制,但是這樣一來所有的action的圖示都是一緻的了,這明顯不符合需求。于是想着用QWidgetAction,自定義一個QWidget,在上面加入icon,菜單文字等,先看下效果:
設計的菜單項的模型如下:
其中,1為放置icon圖檔的widget,2為顯示菜單項文字的Label,3為放置子菜單訓示器的widget。
建立一個繼承QWidget的類,用作QWidgetAction的defaultWidget:
QMenuWidget.h
#pragma once
#include <QWidget>
#include <QMenu>
namespace Ui {
class QMenuWidget;
}
class QMenuWidget : public QWidget
{
Q_OBJECT
public:
QMenuWidget(QWidget *parent = Q_NULLPTR);
~QMenuWidget();
private:
Ui::QMenuWidget *ui;
QWidget *m_icon;
QWidget *m_text;
QWidget *m_submenu_indicator;
public:
void resizeEvent(QResizeEvent *event);
void paintEvent(QPaintEvent *event);
void mouseEnterEvent(QEvent *event);
void mouseLeaveEvent(QEvent *event);
void SetIconWidget(QWidget *widget_);
void SetTextWidget(QWidget *text_);
void SetSubMenuIndicatorWidget(QWidget *indicator_);
void initWidgets();
};
重載resizeEvent是為了安放三個widget的位置;
QMenuWidget.cpp:
#include "QMenuWidget.h"
#include <QPainter>
#include <QStyleOption>
#include <QEnterEvent>
#include <QDebug>
#include <QMouseEvent>
#include "ui_QMenuWidget.h"
QMenuWidget::QMenuWidget(QWidget *parent)
: QWidget(parent)
,m_submenu_indicator(NULL)
,m_icon(NULL)
,m_text(NULL)
,ui(new Ui::QMenuWidget)
{
ui->setupUi(this);
setMouseTracking(true);
}
QMenuWidget::~QMenuWidget()
{
}
void QMenuWidget::resizeEvent(QResizeEvent * event)
{
int icon_w = m_icon->width();
int icon_h = m_icon->height();
int left_margin = 2;
int top_margin = rect().height() - icon_h;
top_margin /= 2;
m_icon->setGeometry(left_margin, top_margin, icon_w, icon_h);
int text_margin_left = 2;
int indicator_w = 0;
if (m_submenu_indicator)
indicator_w = m_submenu_indicator->width();
QRect text_rc;
text_rc.setTop(0);
text_rc.setLeft(rect().left() + left_margin + text_margin_left + icon_w);
text_rc.setRight(rect().right() - indicator_w);
text_rc.setBottom(rect().bottom());
m_text->setGeometry(text_rc);
if (!m_submenu_indicator)
return;
int indicator_h = m_submenu_indicator->height();
top_margin = rect().height() - indicator_h;
top_margin /= 2;
m_submenu_indicator->setGeometry(rect().right() - indicator_w, top_margin, indicator_w, indicator_h);
}
void QMenuWidget::paintEvent(QPaintEvent * event)
{
QStyleOption opt;
opt.init(this);
QPainter p(this);
style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
}
void QMenuWidget::mouseEnterEvent(QEvent * event)
{
bool ishover = this->property("hover").toBool();
if (ishover)
return;
qDebug() << __FUNCTION__;
this->setProperty("hover", true);
m_icon->setProperty("hover", true);
m_text->setProperty("hover", true);
m_icon->style()->unpolish(m_icon);
m_text->style()->unpolish(m_text);
this->style()->unpolish(this);
m_icon->style()->polish(m_icon);
m_text->style()->polish(m_text);
this->style()->polish(this);
m_icon->update();
m_text->update();
this->update();
if (m_submenu_indicator)
{
m_submenu_indicator->setProperty("hover", true);
m_submenu_indicator->style()->unpolish(m_submenu_indicator);
m_submenu_indicator->style()->polish(m_submenu_indicator);
m_submenu_indicator->update();
}
__super::enterEvent(event);
}
void QMenuWidget::mouseLeaveEvent(QEvent * event)
{
bool ishover = this->property("hover").toBool();
if (!ishover)
return;
qDebug() << __FUNCTION__;
this->setProperty("hover", false);
m_icon->setProperty("hover", false);
m_text->setProperty("hover", false);
this->style()->unpolish(this);
m_icon->style()->unpolish(m_icon);
m_text->style()->unpolish(m_text);
this->style()->polish(this);
m_icon->style()->polish(m_icon);
m_text->style()->polish(m_text);
this->update();
m_icon->update();
m_text->update();
if (m_submenu_indicator)
{
m_submenu_indicator->setProperty("hover", false);
m_submenu_indicator->style()->unpolish(m_submenu_indicator);
m_submenu_indicator->style()->polish(m_submenu_indicator);
m_submenu_indicator->update();
}
__super::leaveEvent(event);
}
void QMenuWidget::SetIconWidget(QWidget * widget_)
{
m_icon = widget_;
m_icon->setMouseTracking(true);
}
void QMenuWidget::SetTextWidget(QWidget * text_)
{
m_text = text_;
m_text->setMouseTracking(true);
}
void QMenuWidget::SetSubMenuIndicatorWidget(QWidget * indicator_)
{
m_submenu_indicator = indicator_;
if(m_submenu_indicator)
{
m_submenu_indicator->setMouseTracking(true);
}
}
void QMenuWidget::initWidgets()
{
this->setProperty("hover", false);
m_icon->setProperty("hover", false);
m_text->setProperty("hover", false);
if(m_submenu_indicator)
m_submenu_indicator->setProperty("hover", false);
}
這裡提供了接口來設定icon、label、以及indicator的widget,也可以由QMenuWidget自己來生成這三個widget。這裡記錄一下經驗,最開始的時候我是重載QWidget的enterEvent、leaveEvent來捕捉滑鼠進入以及離開事件,進而改變樣式,後來發現這樣做有幾個問題:
第一個問題,當有子菜單時,滑鼠放在菜單項上時,不能自動彈出子菜單,要點選一下才會彈出;
第二個問題,當有子菜單時,點選彈出子菜單後,滑鼠移開菜單項,leaveEvent不能觸發,進而菜單項一直保留hover=true的狀态,樣式也是hover=true的樣式;
第三個問題,當QWidgetAction以及一個QAction并排放在菜單上時,從QAction移動到QWidgetAction時,QAction的狀态仍為‘selected’狀态;
嘗試了各種方法均無法解決,後來把QMenu的源碼看了下,發現觸發QAction激活是在QMenu::mouseMoveEvent()裡觸發的,子菜單的延時彈出也是在這裡觸發的,于是便做個測試,我在代碼中為QMenu安裝了事件過濾器,發現滑鼠在QMenuWidget上移動時,QMenu并沒有觸發到mouseMoveEvent,便斷定以上的幾個問題,均是因為滑鼠在我自定義的QMenuWidget上移動時,QMenu無法捕捉到mouseMoveEvent。
想要讓QMenu捕捉到QMenuWidget的mouseMove事件,那要怎麼辦呢?上網查了下,子QWidget視窗的事件如果未處理,即沒有重載父類QWidget的事件處理函數,那麼事件就會傳播至父視窗,仔細看了下代碼,發現QMenuWidget并沒有重寫mouseMoveEvent啊,為什麼QMenu不能捕捉到呢,經過調試,終于發現要對QMenuWidget以及icon、label、indicator都設定
setMouseTracking(true);
才能捕捉到mouseMoveEvent并傳播給父視窗QMenu;
這樣之後,第一個問題解決了,第二個、第三個問題仍然存在;經調試,第二個問題是由于子菜單彈出後,滑鼠移開,leaveEvent并沒有觸發;又仔細看了下QMenu的實作代碼,發現每個QAction的狀态都是由QMenu判斷滑鼠事件來決定的,于是決定不在QMenuWidget中判斷各種滑鼠事件,統一由QMenu執行,把裝有QMenuWidget的QWidgetAction當做一般的QAction來處理,最終的QMenuWidget類的代碼就如上述所示。樣式上使用了qss來設定,用到了動态屬性,當滑鼠進入時,設定自定義屬性hover為true,當滑鼠離開時,設定hover為false。主視窗的代碼:
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "QMenuWidget.h"
#include <QFile>
#include <QLabel>
#include <QWidgetAction>
#include <QDebug>
#include <QMouseEvent>
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
setupMenu();
QFile file_(":/qss/res/menu.qss");
file_.open(QFile::ReadOnly);
m_menu->setStyleSheet(file_.readAll());
ui->pushButton->setMenu(m_menu);
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::setupMenu()
{
m_menu = new QMenu(this);
m_menu->setObjectName("menu_1");
QStringList menu_name;
menu_name << "clock" << "map" << "home";
QStringList menu_text;
menu_text << QStringLiteral("時鐘") << QStringLiteral("定位服務") << QStringLiteral("首頁");
for (int i = 0; i < 3;i++)
{
QMenuWidget *mw = new QMenuWidget(this);
mw->setObjectName("menu_widget_" + menu_name.at(i));
mw->setFixedSize(120, 40);
QWidget *icon = new QWidget(mw);
icon->setObjectName("menu_icon_"+menu_name.at(i));
icon->setFixedSize(32, 32);
QLabel *text = new QLabel(mw);
text->setObjectName("menu_text_"+menu_name.at(i));
text->setText(menu_text.at(i));
QWidget* indicator = NULL;
if(menu_name.at(i) == "home")
{
indicator = new QWidget(mw);
indicator->setObjectName("menu_sub_"+menu_name.at(i));
indicator->setFixedSize(8, 12);
}
mw->SetIconWidget(icon);
mw->SetTextWidget(text);
mw->SetSubMenuIndicatorWidget(indicator);
mw->initWidgets();
QWidgetAction *wa = new QWidgetAction(m_menu);
wa->setObjectName("action_" + menu_name.at(i));
wa->setText(menu_name.at(i));
wa->setDefaultWidget(mw);
//mw->SetAction(wa);
m_widget_acts.append(wa);
}
QAction *act3 = new QAction(this);
act3->setText(QStringLiteral("個人首頁"));
QMenu *submenu = new QMenu(this);
submenu->addAction(act3);
m_widget_acts.at(2)->setMenu(submenu);
m_menu->addActions(m_widget_acts);
m_menu->installEventFilter(this);
connect(m_menu, &QMenu::triggered, this, &MainWindow::onMenuTriggered);
}
void MainWindow::onMenuTriggered(QAction *action)
{
qDebug() << __FUNCTION__ << action->text();
}
bool MainWindow::eventFilter(QObject *watched, QEvent *event)
{
if (watched == m_menu)
{
if (event->type() == QEvent::MouseMove)
{
//QMenuWidget *wid = (QMenuWidget *)m_widget_act2->defaultWidget();
QPointF lp = ((QMouseEvent*)event)->localPos();
QList<QAction*>::iterator it = m_widget_acts.begin();
for (it;it != m_widget_acts.end();it++)
{
QWidgetAction *wa = (QWidgetAction*)(*it);
QMenuWidget *wid = (QMenuWidget *)(wa)->defaultWidget();
QRect rc = m_menu->actionGeometry(wa);
if (rc.contains(lp.toPoint()))
{
//qDebug() << rc << lp.toPoint() << wid->geometry();
wid->mouseEnterEvent(event);
}
else
wid->mouseLeaveEvent(event);
}
}
}
return false;
}
這裡為m_menu添加了事件過濾器,滑鼠移動時,判斷滑鼠在哪個action上,然後調用其對應的接口。菜單的qss檔案可參考下方源碼:
Qt滑鼠滑過菜單圖示高亮