剖析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的代码较长, 但比较简单, 都是一些条件判断加上绘图语句, 需要自己修改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的菜单关闭处理方法