剖析QMenu & Qt完全定制化菜单

贴张效果图: 

定制包括: 

1. 周边阴影

2. 菜单项的元素(分割符, 控制ICON大小, 文字显示位置与颜色, 子菜单指示符)

菜单内的效果, 部分可以使用stylesheet实现, 但要做到这样的定制化, stylesheet是做不到的

下面介绍如何实现这些效果: 

 

1. 实现阴影效果

默认的Qt菜单QMenu的效果是这样的

1) 首先需要去除下拉阴影(Drop shadow)

Qt的菜单是继承QWidget然后自绘的, dropshadow不属于自绘范围, 是windows popup类型窗口默认的样式, 无法通过正常途径去除

可以从源码中看到调用过程大概是这样: 

qmenu::popup -> qwidget::show() -> QWidgetPrivate::show_helper() -> show_sys();

  而这时候, 还未调用qmenu::paintevent

而且不能去除QMenu的Popup 属性, 因为QMenu的实现依赖Popup属性, 例如:

  QMenuPrivate::activateAction中使用QApplication::activePopupWidget()函数

在windows平台下:

对窗口的handle操作, 可以去掉drop shadow.  参考http://stackoverflow.com/questions/13776119/qt-menu-without-shaodw

menu.h

#ifndef MENU_H
#define MENU_H

#include <QMenu>

class Menu : public QMenu
{
    Q_OBJECT
public:
    explicit Menu(QWidget *parent = 0);
    explicit Menu(const QString & title);

protected:
    virtual bool event(QEvent *event);

signals:

public slots:

};

#endif // MENU_H

menu.cpp

 

#include "menu.h"


Menu::Menu(QWidget *parent) :
    QMenu(parent)
{

}

Menu::Menu(const QString &title) :
    QMenu(title)
{

}



bool Menu::event(QEvent *event)
{
    static bool class_amended = false;
    if (event->type() == QEvent::WinIdChange)
    {
        HWND hwnd = reinterpret_cast<HWND>(winId());
        if (class_amended == false)
        {
            class_amended = true;
            DWORD class_style = ::GetClassLong(hwnd, GCL_STYLE);
            class_style &= ~CS_DROPSHADOW;
            ::SetClassLong(hwnd, GCL_STYLE, class_style);
        }
       
    }
    return QWidget::event(event);
}

大概思路是: 在event中截获QEvent::WinIdChange事件, 然后获得窗口handle,  使用GetClassLong / SetClassLong 去除 CS_DROPSHADOW flags, 即可去除阴影

 

2) 使用dwm实现环绕阴影

优点:系统内置支持

缺点: 仅在vista以上并开启aero特效的情况, 使菜单有阴影环绕.

 

#pragma comment( lib, "dwmapi.lib" )
#include "dwmapi.h"
bool Menu::event(QEvent *event)
{
    static bool class_amended = false;
    if (event->type() == QEvent::WinIdChange)
    {
        HWND hwnd = reinterpret_cast<HWND>(winId());
        if (class_amended == false)
        {
            class_amended = true;
            DWORD class_style = ::GetClassLong(hwnd, GCL_STYLE);
            class_style &= ~CS_DROPSHADOW;
            ::SetClassLong(hwnd, GCL_STYLE, class_style);
        }
        DWMNCRENDERINGPOLICY val = DWMNCRP_ENABLED;
        ::DwmSetWindowAttribute(hwnd, DWMWA_NCRENDERING_POLICY, &val, sizeof(DWMNCRENDERINGPOLICY));

        // This will turn OFF the shadow
        // MARGINS m = {0};
        // This will turn ON the shadow
        MARGINS m = {-1};
        HRESULT hr = ::DwmExtendFrameIntoClientArea(hwnd, &m);
        if( SUCCEEDED(hr) )
        {
            //do more things
        }
    }
    return QWidget::event(event);
}

简单地修改一下event的实现即可

3) 手动绘制阴影

1. CCustomMenu 继承 QMenu

void CCustomMenu::_Init()
{
    // 必须设置popup, 因为QMenuPrivate::activateAction中使用QApplication::activePopupWidget()函数
    this->setWindowFlags(Qt::Popup | Qt::FramelessWindowHint); 
    this->setAttribute(Qt::WA_TranslucentBackground); 
    this->setObjectName("CustomMenu");  // 以objectname 区分Qt内置菜单和CCustomMenu

}

设置菜单背景透明

objectname是为了在绘制时区分不同风格的菜单(比如原生Qmenu与CCustomMenu或者其他CCustomMenu2等)

 

2. 实现CCustomStyle (参考Qt的源码 QFusionStyle)

CCustomStyle继承自QProxyStyle, Qt控件中的基础元素都是通过style控制, style比stylesheet更底层, 可以做到更精细的控制

/**@brief 定制菜单style
    @author  lwh 
*/
class CCustomStyle : public QProxyStyle
{
    Q_OBJECT

public:
    CCustomStyle(QStyle *style = 0); 

    void drawControl(ControlElement control, const QStyleOption *option,
        QPainter *painter, const QWidget *widget) const;

    void drawPrimitive(PrimitiveElement element, const QStyleOption *option,
        QPainter *painter, const QWidget *widget) const; 

    int pixelMetric ( PixelMetric pm, const QStyleOption * opt, const QWidget * widget) const; 

private:
    void _DrawMenuItem(const QStyleOption *option,
        QPainter *painter, const QWidget *widget) const; 
    QPixmap     _pixShadow     ; //阴影图片    
};

首先需要调整菜单项与边框的距离, 用于绘制阴影

在pixelMetric 中添加

    if(pm == PM_MenuPanelWidth) 
        return 6;        // 调整边框宽度, 以绘制阴影

pixelMetric 中描述了像素公制可取的一些值,一个像素公制值是单个像素在样式中表现的尺寸. 

然后再drawPrimitive实现阴影绘制

void CCustomStyle::drawPrimitive( PrimitiveElement element, const QStyleOption *option, QPainter *painter, const QWidget *widget ) const
{
    if(element == PE_FrameMenu)
    {
        painter->save();
        {
            if(_pixShadow.isNull() 
                || widget->objectName() != "CustomMenu")  // fix bug: Qt的内置菜单显示不正常(如TextEdit右键菜单)
            {
                painter->restore();    
                return __super::drawPrimitive(element, option, painter, widget); 
            }

            QSize szThis = option->rect.size();
            QPixmap pixShadowBg = _DrawNinePatch(szThis, _pixShadow); 
            painter->drawPixmap(option->rect, pixShadowBg);                
        }
        painter->restore();    
        return; 
    }
    __super::drawPrimitive(element, option, painter, widget); 
}

QStyle::PE_FrameMenu      Frame for popup windows/menus; see also QMenu.

注意: 绘制完直接return

_DrawNinePatch是以九宫格形式绘制, 将这样一张小的阴影图绘制到窗口时, 如果直接拉伸, 会变得非常模糊. 

而九宫格形式可以绘制出相对漂亮的背景, 这种技巧同样可以应用在其他控件上. 

const QPixmap _DrawNinePatch( QSize szDst, const QPixmap &srcPix )
{
    // 绘制背景图到, 以九宫格形式

    QPixmap dstPix(szDst); 
    dstPix.fill(QColor(255, 255, 255, 0));
    QPainter painter;
    painter.begin(&dstPix);

    int nW = szDst.width();
    int nH = szDst.height();

    int nWBg = srcPix.width();
    int nHBg = srcPix.height();
    QPoint m_ptBgLT(10, 10); 
    QPoint m_ptBgRB(19, 19); 

    QPoint ptDstLT(m_ptBgLT.x(), m_ptBgLT.y());
    QPoint ptDstRB(nW-(nWBg-m_ptBgRB.x()), nH-(nHBg-m_ptBgRB.y()));

    //LT
    painter.drawPixmap(QRect(0,0,ptDstLT.x(), ptDstLT.y()), srcPix, QRect(0,0,m_ptBgLT.x(), m_ptBgLT.y()));
    //MT
    painter.drawPixmap(QRect(ptDstLT.x(),0, ptDstRB.x()-ptDstLT.x(), ptDstLT.y()), srcPix, QRect(m_ptBgLT.x(),0,m_ptBgRB.x()-m_ptBgLT.x(), m_ptBgLT.y()));
    //RT
    painter.drawPixmap(QRect(ptDstRB.x(),0,nW-ptDstRB.x(), ptDstLT.y()), srcPix, QRect(m_ptBgRB.x(),0,nWBg-m_ptBgRB.x(), m_ptBgLT.y()));
    //LM
    painter.drawPixmap(QRect(0,ptDstLT.y(),ptDstLT.x(), ptDstRB.y()-ptDstLT.y()), srcPix, QRect(0,m_ptBgLT.y(),m_ptBgLT.x(), m_ptBgRB.y()-m_ptBgLT.y()));
    //MM
    painter.drawPixmap(QRect(ptDstLT.x(),ptDstLT.y(),ptDstRB.x()-ptDstLT.x(), ptDstRB.y()-ptDstLT.y()), srcPix, QRect(m_ptBgLT.x(),m_ptBgLT.y(),m_ptBgRB.x()-m_ptBgLT.x(), m_ptBgRB.y()-m_ptBgLT.y()));
    //RM
    painter.drawPixmap(QRect(ptDstRB.x(),ptDstLT.y(), nW-ptDstRB.x(), ptDstRB.y()-ptDstLT.y()), srcPix, QRect(m_ptBgRB.x(),m_ptBgLT.y(), nWBg-m_ptBgRB.x(), m_ptBgRB.y()-m_ptBgLT.y()));
    //LB
    painter.drawPixmap(QRect(0,ptDstRB.y(),ptDstLT.x(), nH-ptDstRB.y()), srcPix, QRect(0,m_ptBgRB.y(),m_ptBgLT.x(), nHBg-m_ptBgRB.y()));
    //MB
    painter.drawPixmap(QRect(ptDstLT.x(),ptDstRB.y(),ptDstRB.x()-ptDstLT.x(),  nH-ptDstRB.y()), srcPix, QRect(m_ptBgLT.x(),m_ptBgRB.y(),m_ptBgRB.x()-m_ptBgLT.x(),  nHBg-m_ptBgRB.y()));
    //RB
    painter.drawPixmap(QRect(ptDstRB.x(),ptDstRB.y(),nW-ptDstRB.x(),  nH-ptDstRB.y()), srcPix, QRect(m_ptBgRB.x(),m_ptBgRB.y(),nWBg-m_ptBgRB.x(),  nHBg-m_ptBgRB.y()));

    painter.end(); 
    return dstPix; 
}

2.  绘制菜单项

1) 控制ICON大小

在pixelMetric中:

    if (pm == QStyle::PM_SmallIconSize) 
        return 12;    //返回ICON的大小    

2) 绘制菜单项内容

void CCustomStyle::drawControl( ControlElement control, const QStyleOption *option, QPainter *painter, const QWidget *widget ) const
{
    switch(control )
    {
    case CE_MenuItem:
        {
            _DrawMenuItem(option, painter, widget);
            return;            // 直接返回, 否则会被super::drawcontrol覆盖
        }
    }
    __super::drawControl(control, option, painter, widget); 
}
  1 void CCustomStyle::_DrawMenuItem(const QStyleOption *option, QPainter *painter, const QWidget *widget ) const
  2 {
  3     painter->save();
  4 
  5     if (const QStyleOptionMenuItem *menuItem = qstyleoption_cast<const QStyleOptionMenuItem *>(option))
  6     {
  7         // 先绘制一层背景(否则在透明情况下, 会直接透过去);
  8         painter->setPen(colItemBg); 
  9         painter->setBrush(colItemBg); 
 10         painter->drawRect(option->rect); 
 11 
 12         if (menuItem->menuItemType == QStyleOptionMenuItem::Separator) {
 13             int w = 0;
 14             if (!menuItem->text.isEmpty()) {                // 绘制分隔符文字
 15                 painter->setFont(menuItem->font);
 16                 proxy()->drawItemText(painter, menuItem->rect.adjusted(5, 0, -5, 0), Qt::AlignLeft | Qt::AlignVCenter,
 17                     menuItem->palette, menuItem->state & State_Enabled, menuItem->text,
 18                     QPalette::Text);
 19                 w = menuItem->fontMetrics.width(menuItem->text) + 5;
 20             }
 21             painter->setPen(colSeparator);
 22             bool reverse = menuItem->direction == Qt::RightToLeft;
 23             painter->drawLine(menuItem->rect.left() + 5 + (reverse ? 0 : w), menuItem->rect.center().y(),
 24                 menuItem->rect.right() - 5 - (reverse ? w : 0), menuItem->rect.center().y());
 25             painter->restore();
 26             return;
 27         }
 28         bool selected = menuItem->state & State_Selected && menuItem->state & State_Enabled;
 29         if (selected) {
 30             QRect r = option->rect;
 31             painter->fillRect(r, colItemHighlight);
 32         }
 33         bool checkable = menuItem->checkType != QStyleOptionMenuItem::NotCheckable;
 34         bool checked = menuItem->checked;
 35         bool sunken = menuItem->state & State_Sunken;
 36         bool enabled = menuItem->state & State_Enabled;
 37 
 38         bool ignoreCheckMark = false;
 39         int checkcol = qMax(menuItem->maxIconWidth, 20);
 40 
 41         if (qobject_cast<const QComboBox*>(widget))
 42             ignoreCheckMark = true; //ignore the checkmarks provided by the QComboMenuDelegate
 43 
 44         if (!ignoreCheckMark) {
 45             // Check
 46             QRect checkRect(option->rect.left() + 7, option->rect.center().y() - 6, 14, 14);
 47             checkRect = visualRect(menuItem->direction, menuItem->rect, checkRect);
 48             if (checkable) {
 49                 if (menuItem->checkType & QStyleOptionMenuItem::Exclusive) {
 50                     // Radio button 未实现
 51                     if (checked || sunken) {
 52                     /*    painter->setRenderHint(QPainter::Antialiasing);
 53                         painter->setPen(Qt::NoPen);
 54 
 55                         QPalette::ColorRole textRole = !enabled ? QPalette::Text:
 56                             selected ? QPalette::HighlightedText : QPalette::ButtonText;
 57                         painter->setBrush(option->palette.brush( option->palette.currentColorGroup(), textRole));
 58                         painter->drawEllipse(checkRect.adjusted(4, 4, -4, -4));
 59                         */
 60                     }
 61                 } else {
 62                     // Check box
 63                     if (menuItem->icon.isNull()) {
 64                         QStyleOptionButton box;
 65                         box.QStyleOption::operator=(*option);
 66                         box.rect = checkRect;
 67                         if (checked)
 68                             box.state |= State_On;
 69                         proxy()->drawPrimitive(PE_IndicatorCheckBox, &box, painter, widget);
 70                         
 71                     }
 72                 }
 73             }
 74         } else { //ignore checkmark
 75             if (menuItem->icon.isNull())
 76                 checkcol = 0;
 77             else
 78                 checkcol = menuItem->maxIconWidth;
 79         }
 80 
 81         // Text and icon, ripped from windows style
 82         bool dis = !(menuItem->state & State_Enabled);
 83         bool act = menuItem->state & State_Selected;
 84         const QStyleOption *opt = option;
 85         const QStyleOptionMenuItem *menuitem = menuItem;
 86 
 87         QPainter *p = painter;
 88         QRect vCheckRect = visualRect(opt->direction, menuitem->rect,
 89             QRect(menuitem->rect.x() + 4, menuitem->rect.y(),
 90             checkcol, menuitem->rect.height()));
 91         if (!menuItem->icon.isNull()) {
 92             QIcon::Mode mode = dis ? QIcon::Disabled : QIcon::Normal;
 93             if (act && !dis)
 94                 mode = QIcon::Active;
 95             QPixmap pixmap;
 96 
 97             int smallIconSize = proxy()->pixelMetric(PM_SmallIconSize, option, widget);
 98             QSize iconSize(smallIconSize, smallIconSize);
 99             if (const QComboBox *combo = qobject_cast<const QComboBox*>(widget))
100                 iconSize = combo->iconSize();
101             if (checked)
102                 pixmap = menuItem->icon.pixmap(iconSize, mode, QIcon::On);
103             else
104                 pixmap = menuItem->icon.pixmap(iconSize, mode);
105 
106             int pixw = pixmap.width();
107             int pixh = pixmap.height();
108 
109             QRect pmr(0, 0, pixw, pixh);
110             pmr.moveCenter(vCheckRect.center());
111             painter->setPen(colText);//menuItem->palette.text().color()
112             if (checkable && checked) {
113                 QStyleOption opt = *option;
114                 if (act) {
115                     QColor activeColor = mergedColors(
116                         colItemBg, //option->palette.background().color(),
117                         colItemHighlight // option->palette.highlight().color());
118                         ); 
119                     opt.palette.setBrush(QPalette::Button, activeColor);
120                 }
121                 opt.state |= State_Sunken;
122                 opt.rect = vCheckRect;
123                 proxy()->drawPrimitive(PE_PanelButtonCommand, &opt, painter, widget);
124             }
125             painter->drawPixmap(pmr.topLeft(), pixmap);
126         }
127         if (selected) {
128             painter->setPen(colText);//menuItem->palette.highlightedText().color()
129         } else {
130             painter->setPen(colText); //menuItem->palette.text().color()
131         }
132         int x, y, w, h;
133         menuitem->rect.getRect(&x, &y, &w, &h);
134         int tab = menuitem->tabWidth;
135         QColor discol;
136         if (dis) {
137             discol = colDisText; //menuitem->palette.text().color()
138             p->setPen(discol);
139         }
140         int xm = windowsItemFrame + checkcol + windowsItemHMargin + 2;
141         int xpos = menuitem->rect.x() + xm;
142 
143         QRect textRect(xpos, y + windowsItemVMargin, w - xm - windowsRightBorder - tab + 1, h - 2 * windowsItemVMargin);
144         QRect vTextRect = visualRect(opt->direction, menuitem->rect, textRect);
145         QString s = menuitem->text;
146         if (!s.isEmpty()) {                     // draw text
147             p->save();
148             int t = s.indexOf(QLatin1Char('\t'));
149             int text_flags = Qt::AlignVCenter | Qt::TextShowMnemonic | Qt::TextDontClip | Qt::TextSingleLine;
150             if (!__super::styleHint(SH_UnderlineShortcut, menuitem, widget))
151                 text_flags |= Qt::TextHideMnemonic;
152             text_flags |= Qt::AlignLeft;
153             if (t >= 0) {
154                 QRect vShortcutRect = visualRect(opt->direction, menuitem->rect,
155                     QRect(textRect.topRight(), QPoint(menuitem->rect.right(), textRect.bottom())));
156                 if (dis && !act && proxy()->styleHint(SH_EtchDisabledText, option, widget)) {
157                     p->setPen(colText);//menuitem->palette.light().color()
158                     p->drawText(vShortcutRect.adjusted(1, 1, 1, 1), text_flags, s.mid(t + 1));
159                     p->setPen(discol);
160                 }
161                 p->drawText(vShortcutRect, text_flags, s.mid(t + 1));
162                 s = s.left(t);
163             }
164             QFont font = menuitem->font;
165             // font may not have any "hard" flags set. We override
166             // the point size so that when it is resolved against the device, this font will win.
167             // This is mainly to handle cases where someone sets the font on the window
168             // and then the combo inherits it and passes it onward. At that point the resolve mask
169             // is very, very weak. This makes it stonger.
170             font.setPointSizeF(QFontInfo(menuItem->font).pointSizeF());
171 
172             if (menuitem->menuItemType == QStyleOptionMenuItem::DefaultItem)
173                 font.setBold(true);
174 
175             p->setFont(font);
176             if (dis && !act && proxy()->styleHint(SH_EtchDisabledText, option, widget)) {
177                 p->setPen(menuitem->palette.light().color()); 
178                 p->drawText(vTextRect.adjusted(1, 1, 1, 1), text_flags, s.left(t));
179                 p->setPen(discol);
180             }
181             p->drawText(vTextRect, text_flags, s.left(t));
182             p->restore();
183         }
184 
185         // Arrow 绘制子菜单指示符
186         if (menuItem->menuItemType == QStyleOptionMenuItem::SubMenu) {// draw sub menu arrow
187             int dim = (menuItem->rect.height() - 4) / 2;
188             PrimitiveElement arrow;
189             arrow = option->direction == Qt::RightToLeft ? PE_IndicatorArrowLeft : PE_IndicatorArrowRight;
190             int xpos = menuItem->rect.left() + menuItem->rect.width() - 3 - dim;
191             QRect  vSubMenuRect = visualRect(option->direction, menuItem->rect,
192                 QRect(xpos, menuItem->rect.top() + menuItem->rect.height() / 2 - dim / 2, dim, dim));
193             QStyleOptionMenuItem newMI = *menuItem;
194             newMI.rect = vSubMenuRect;
195             newMI.state = !enabled ? State_None : State_Enabled;
196             if (selected)
197                 newMI.palette.setColor(QPalette::ButtonText,                                        // 此处futionstyle 有误, QPalette::Foreground改为ButtonText
198                     colIndicatorArrow);//newMI.palette.highlightedText().color()
199             else
200                 newMI.palette.setColor(QPalette::ButtonText,
201                     colIndicatorArrow);
202 
203             proxy()->drawPrimitive(arrow, &newMI, painter, widget);
204         }
205     }
206     painter->restore();    
207 }
_DrawMenuItem

 


_DrawMenuItem的代码较长,  但比较简单, 都是一些条件判断加上绘图语句, 需要自己修改pallete的颜色

值得注意的是: 在透明情况下, 应先绘制一层menu item 的背景, 否则会直接透过去

 

3) 最后还要重写一下QMenu的addMenu

以使子菜单也生效

QAction * CCustomMenu::addMenu( CCustomMenu *menu )
{
    return QMenu::addMenu(menu); 
}

CCustomMenu * CCustomMenu::addMenu( const QString &title )
{
    CCustomMenu *menu = new CCustomMenu(title, this);
    addAction(menu->menuAction());
    return menu;
}

CCustomMenu * CCustomMenu::addMenu( const QIcon &icon, const QString &title )
{
    CCustomMenu *menu = new CCustomMenu(title, this);
    menu->setIcon(icon);
    addAction(menu->menuAction());
    return menu;
}

 

完整的工程代码在此, https://bitbucket.org/lingdhox/misc/src 或者CSDN http://download.csdn.net/detail/l470080245/6731989

编译需要VS2010+Qt5. 

PS: 

关于QMenu如何处理菜单消失, 参考我的另一篇blog Qt中QMenu的菜单关闭处理方法

 

posted on 2013-12-18 01:26  一 水  阅读(31218)  评论(2编辑  收藏  举报

导航