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

贴张效果图: 剖析QMenu & Qt完全定制化菜单

定制包括: 

1. 周边阴影

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

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

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

 

1. 实现阴影效果

默认的Qt菜单QMenu的效果是这样的剖析QMenu & Qt完全定制化菜单

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的菜单关闭处理方法

 

你可能感兴趣的:(menu)