• 剖析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('	'));
    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的菜单关闭处理方法

  • 相关阅读:
    今天学到的单词
    今天是运维的一天
    今天是属于数据库的一天
    Python基础9 元组的访问和拆包
    Python基础8 元组的创建
    Python基础7 序列
    Python基础6 控制语句 if else elif range() while for
    Python基础5 运算符
    Python基础4 字符串的查找 find rfind 字符串类型和数字类型的转换
    Python基础2 数据类型:数字类型
  • 原文地址:https://www.cnblogs.com/lingdhox/p/3479568.html
Copyright © 2020-2023  润新知