QWidgetAction實現滑鼠滑過選單項圖示高亮顯示
需求是滑鼠滑過選單項時,選單項的文字、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檔案可參考下方原始碼: