1. 程式人生 > >QWidgetAction實現滑鼠滑過選單項圖示高亮顯示

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檔案可參考下方原始碼: