#include "SARibbonToolButton.h" #include "SARibbonPannel.h" #include #include #include #include #include #include #include #include #include #include #include #include #include /** * @def 定义文字换行时2行文本的矩形高度系数,此系数决定文字区域的高度 * * fontMetrics.lineSpacing*系数 = 文本区域高度 */ #define SARIBBONTOOLBUTTON_WORDWRAP_TEXT_FACTOR 2.05 /** * @def 定义文字不换行时单行文本的矩形高度系数,此系数决定文字区域的高度 * * fontMetrics.lineSpacing*系数 = 文本区域高度 */ #define SARIBBONTOOLBUTTON_NOWORDWRAP_TEXT_FACTOR 1.2 /** * @def 定义小按钮的矩形高度系数,此系数决定文字区域的高度 * * fontMetrics.lineSpacing*系数 = 文本区域高度 */ #define SARIBBONTOOLBUTTON_SMALLBUTTON_TEXT_FACTOR 1.4 /** * @def 文本宽度估算时的宽度比高度系数 * * 超过此系数的宽度时,开始尝试换行,例如按钮高度为h,如果单行文本的宽度大于h*系数,则触发换行估算 */ #define SARIBBONTOOLBUTTON_WORDWRAP_WIDTH_PER_HEIGHT_RATIO 1.4 /** * @def 开启此宏会打印一些常见信息 */ #define SA_RIBBON_TOOLBUTTON_DEBUG_PRINT 0 #define SARIBBONTOOLBUTTON_DEBUG_DRAW 0 #if SARIBBONTOOLBUTTON_DEBUG_DRAW #define SARIBBONTOOLBUTTON_DEBUG_DRAW_RECT(p, rect) \ do { \ p.save(); \ p.setPen(Qt::red); \ p.setBrush(QBrush()); \ p.drawRect(rect); \ p.restore(); \ } while (0) #else #define SARIBBONTOOLBUTTON_DEBUG_DRAW_RECT(p, rect) #endif namespace SA { QDebug operator<<(QDebug debug, const QStyleOptionToolButton& opt) { debug << "==============" << "\nQStyleOption(" << (QStyleOption)opt << ")" << "\n QStyleOptionComplex:" "\n subControls(" << opt.subControls << " ) " "\n activeSubControls(" << opt.activeSubControls << "\n QStyleOptionToolButton" "\n features(" << opt.features << ")" "\n toolButtonStyle(" << opt.toolButtonStyle << ")"; return (debug); } } //=================================================== // SARibbonToolButtonProxyStyle //=================================================== class SARibbonToolButtonProxyStyle : public QProxyStyle { public: void drawPrimitive(PrimitiveElement pe, const QStyleOption* opt, QPainter* p, const QWidget* widget = nullptr) const override { if (pe == PE_IndicatorArrowUp || pe == PE_IndicatorArrowDown || pe == PE_IndicatorArrowRight || pe == PE_IndicatorArrowLeft) { if (opt->rect.width() <= 1 || opt->rect.height() <= 1) return; QRect r = opt->rect; int size = qMin(r.height(), r.width()); QPixmap pixmap; qreal pixelRatio = p->device()->devicePixelRatio(); int border = qRound(pixelRatio * (size / 4)); int sqsize = qRound(pixelRatio * (2 * (size / 2))); QImage image(sqsize, sqsize, QImage::Format_ARGB32_Premultiplied); image.fill(Qt::transparent); QPainter imagePainter(&image); QPolygon a; switch (pe) { case PE_IndicatorArrowUp: a.setPoints(3, border, sqsize / 2, sqsize / 2, border, sqsize - border, sqsize / 2); break; case PE_IndicatorArrowDown: a.setPoints(3, border, sqsize / 2, sqsize / 2, sqsize - border, sqsize - border, sqsize / 2); break; case PE_IndicatorArrowRight: a.setPoints(3, sqsize - border, sqsize / 2, sqsize / 2, border, sqsize / 2, sqsize - border); break; case PE_IndicatorArrowLeft: a.setPoints(3, border, sqsize / 2, sqsize / 2, border, sqsize / 2, sqsize - border); break; default: break; } int bsx = 0; int bsy = 0; if (opt->state & State_Sunken) { bsx = proxy()->pixelMetric(PM_ButtonShiftHorizontal, opt, widget); bsy = proxy()->pixelMetric(PM_ButtonShiftVertical, opt, widget); } QRect bounds = a.boundingRect(); int sx = sqsize / 2 - bounds.center().x() - 1; int sy = sqsize / 2 - bounds.center().y() - 1; imagePainter.translate(sx + bsx, sy + bsy); imagePainter.setPen(QPen(opt->palette.buttonText().color(), 1.4)); imagePainter.setBrush(Qt::NoBrush); if (!(opt->state & State_Enabled)) { imagePainter.translate(1, 1); imagePainter.setPen(QPen(opt->palette.light().color(), 1.4)); imagePainter.drawPolyline(a); imagePainter.translate(-1, -1); imagePainter.setPen(QPen(opt->palette.mid().color(), 1.4)); } imagePainter.drawPolyline(a); imagePainter.end(); pixmap = QPixmap::fromImage(image); pixmap.setDevicePixelRatio(pixelRatio); int xOffset = r.x() + (r.width() - size) / 2; int yOffset = r.y() + (r.height() - size) / 2; p->drawPixmap(xOffset, yOffset, pixmap); } else { QProxyStyle::drawPrimitive(pe, opt, p, widget); } } }; //=================================================== // SARibbonToolButton::PrivateData //=================================================== class SARibbonToolButton::PrivateData { SA_RIBBON_DECLARE_PUBLIC(SARibbonToolButton) public: PrivateData(SARibbonToolButton* p); // 根据鼠标位置更新按钮的信息 void updateStatusByMousePosition(const QPoint& pos); // 更新绘图相关的尺寸 void updateDrawRect(const QStyleOptionToolButton& opt); // 更新SizeHint void updateSizeHint(const QStyleOptionToolButton& opt); // 计算涉及到的rect尺寸 void calcDrawRects(const QStyleOptionToolButton& opt, QRect& iconRect, QRect& textRect, QRect& indicatorArrowRect, int spacing, int indicatorLen) const; // 计算小按钮模式下的尺寸 void calcSmallButtonDrawRects(const QStyleOptionToolButton& opt, QRect& iconRect, QRect& textRect, QRect& indicatorArrowRect, int spacing, int indicatorLen) const; // 计算大按钮模式下的尺寸 void calcLargeButtonDrawRects(const QStyleOptionToolButton& opt, QRect& iconRect, QRect& textRect, QRect& indicatorArrowRect, int spacing, int indicatorLen) const; // 根据按钮的尺寸调节iconsize(注意这里的buttonRect是已经减去mSpacing的情况) QSize adjustIconSize(const QRect& buttonRect, const QSize& originIconSize) const; // 判断是否有Indicator bool hasIndicator(const QStyleOptionToolButton& opt) const; // 计算sizehint QSize calcSizeHint(const QStyleOptionToolButton& opt); QSize calcSmallButtonSizeHint(const QStyleOptionToolButton& opt); QSize calcLargeButtonSizeHint(const QStyleOptionToolButton& opt); // 计算文本绘制矩形的高度 int calcTextDrawRectHeight(const QStyleOptionToolButton& opt) const; // 估算一个最优的文本宽度 int estimateLargeButtonTextWidth(int buttonHeight, int textDrawRectHeight, const QString& text, const QFontMetrics& fm, float widthHeightRatio = SARIBBONTOOLBUTTON_WORDWRAP_WIDTH_PER_HEIGHT_RATIO, int maxTrycount = 3); QPixmap createIconPixmap(const QStyleOptionToolButton& opt, const QSize& iconsize) const; // 获取文字的对其方式 int getTextAlignment() const; // 确认文字是否确切要换行显示 bool isTextNeedWrap() const; // 仅仅对\n进行剔除,和QString::simplified不一样 static QString simplified(const QString& str); public: bool mMouseOnSubControl { false }; ///< 这个用于标记MenuButtonPopup模式下,鼠标在文本区域 bool mMenuButtonPressed { false }; ///< 由于Indicator改变,因此hitButton不能用QToolButton的hitButton bool mWordWrap { false }; ///< 标记是否文字换行 @default false SARibbonToolButton::RibbonButtonType mButtonType { SARibbonToolButton::LargeButton }; int mSpacing { 1 }; ///< 按钮和边框的距离 int mIndicatorLen { 8 }; ///< Indicator的长度 QRect mDrawIconRect; ///< 记录icon的绘制位置 QRect mDrawTextRect; ///< 记录text的绘制位置 QRect mDrawIndicatorArrowRect; ///< 记录IndicatorArrow的绘制位置 QSize mSizeHint; ///< 保存计算好的sizehint bool mIsTextNeedWrap { false }; ///< 标记文字是否需要换行显示 public: static bool s_enableWordWrap; ///< 在lite模式下是否允许文字换行,如果允许,则图标相对比较小,默认不允许 }; // 静态参数初始化 bool SARibbonToolButton::PrivateData::s_enableWordWrap = false; SARibbonToolButton::PrivateData::PrivateData(SARibbonToolButton* p) : q_ptr(p) { auto proxy = new SARibbonToolButtonProxyStyle(); proxy->setParent(p); // take ownership to avoid memleak p->setStyle(proxy); } /** * @brief 根据鼠标的位置更新状态,主要用于判断鼠标是否位于subcontrol * * 此函数主要应用在action menu模式下 * @param pos */ void SARibbonToolButton::PrivateData::updateStatusByMousePosition(const QPoint& pos) { bool isMouseOnSubControl(false); if (SARibbonToolButton::LargeButton == mButtonType) { isMouseOnSubControl = mDrawTextRect.united(mDrawIndicatorArrowRect).contains(pos); } else { // 小按钮模式就和普通toolbutton一样 isMouseOnSubControl = mDrawIndicatorArrowRect.contains(pos); } if (mMouseOnSubControl != isMouseOnSubControl) { mMouseOnSubControl = isMouseOnSubControl; // 从icon变到text过程中刷新一次 q_ptr->update(); } } /** * @brief 更新绘图的几个关键尺寸 * * 包括: * * - DrawIconRect 绘制图标的矩形区域 * * - DrawTextRect 绘制文本的矩形区域 * * - DrawIndicatorArrowRect 绘制菜单下箭头的矩形区域 * * @param opt */ void SARibbonToolButton::PrivateData::updateDrawRect(const QStyleOptionToolButton& opt) { if (!mSizeHint.isValid()) { updateSizeHint(opt); } // 先更新IndicatorLen mIndicatorLen = q_ptr->style()->pixelMetric(QStyle::PM_MenuButtonIndicator, &opt, q_ptr); if (mIndicatorLen < 3) { if (SARibbonToolButton::LargeButton == mButtonType) { mIndicatorLen = 8; } else { mIndicatorLen = 12; // 小按钮模式下设置为10 } } calcDrawRects(opt, mDrawIconRect, mDrawTextRect, mDrawIndicatorArrowRect, mSpacing, mIndicatorLen); } /** * @brief 更新sizehint * @param opt */ void SARibbonToolButton::PrivateData::updateSizeHint(const QStyleOptionToolButton& opt) { mSizeHint = calcSizeHint(opt); } /** * @brief 计算绘图的几个关键区域 * @param opt * @param iconRect 绘制图标的矩形区域 * @param textRect 绘制文本的矩形区域 * @param indicatorArrowRect 绘制菜单下箭头的矩形区域 * @param spacing * @param indicatorLen */ void SARibbonToolButton::PrivateData::calcDrawRects(const QStyleOptionToolButton& opt, QRect& iconRect, QRect& textRect, QRect& indicatorArrowRect, int spacing, int indicatorLen) const { if (SARibbonToolButton::LargeButton == mButtonType) { calcLargeButtonDrawRects(opt, iconRect, textRect, indicatorArrowRect, spacing, indicatorLen); } else { calcSmallButtonDrawRects(opt, iconRect, textRect, indicatorArrowRect, spacing, indicatorLen); } } /** * @brief 计算小按钮模式下的绘制尺寸 * @param opt * @param iconRect * @param textRect * @param indicatorArrowRect * @param spacing * @param indicatorLen */ void SARibbonToolButton::PrivateData::calcSmallButtonDrawRects(const QStyleOptionToolButton& opt, QRect& iconRect, QRect& textRect, QRect& indicatorArrowRect, int spacing, int indicatorLen) const { switch (opt.toolButtonStyle) { case Qt::ToolButtonIconOnly: { if (hasIndicator(opt)) { // 在仅有图标的小模式显示时,预留一个下拉箭头位置 iconRect = opt.rect.adjusted(spacing, spacing, -indicatorLen - spacing, -spacing); indicatorArrowRect = QRect(opt.rect.right() - indicatorLen - spacing, iconRect.y(), indicatorLen, iconRect.height()); } else { iconRect = opt.rect.adjusted(spacing, spacing, -spacing, -spacing); indicatorArrowRect = QRect(); } // 文本区域为空 textRect = QRect(); } break; case Qt::ToolButtonTextOnly: { if (hasIndicator(opt)) { // 在仅有图标的小模式显示时,预留一个下拉箭头位置 textRect = opt.rect.adjusted(spacing, spacing, -indicatorLen - spacing, -spacing); indicatorArrowRect = QRect(opt.rect.right() - indicatorLen - spacing, spacing, indicatorLen, textRect.height()); } else { textRect = opt.rect.adjusted(spacing, spacing, -spacing, -spacing); indicatorArrowRect = QRect(); } // 绘图区域为空 iconRect = QRect(); } break; default: { bool hasInd = hasIndicator(opt); // icon Beside和under都是一样的 QRect buttonRect = q_ptr->rect(); buttonRect.adjust(spacing, spacing, -spacing, -spacing); // 先设置IconRect if (opt.icon.isNull()) { // 没有图标 iconRect = QRect(); } else { QSize iconSize = adjustIconSize(buttonRect, opt.iconSize); iconRect = QRect(buttonRect.x(), buttonRect.y(), iconSize.width(), qMax(iconSize.height(), buttonRect.height())); } // 后设置TextRect if (opt.text.isEmpty()) { textRect = QRect(); } else { // 分有菜单和没菜单两种情况 int adjx = iconRect.isValid() ? (iconRect.width() + spacing) : 0; // 在buttonRect上变换,因此如果没有图标是不用偏移spacing if (hasInd) { textRect = buttonRect.adjusted(adjx, 0, -indicatorLen, 0); } else { textRect = buttonRect.adjusted(adjx, 0, 0, 0); // 在buttonRect上变换,因此如果没有图标是不用偏移spacing } } // 最后设置Indicator if (hasInd) { if (textRect.isValid()) { indicatorArrowRect = QRect(buttonRect.right() - indicatorLen + 1, textRect.y(), indicatorLen, textRect.height()); } else if (iconRect.isValid()) { indicatorArrowRect = QRect(buttonRect.right() - indicatorLen + 1, iconRect.y(), indicatorLen, iconRect.height()); } else { indicatorArrowRect = buttonRect; } } else { indicatorArrowRect = QRect(); } } } } /** * @brief 计算大按钮模式下的绘制尺寸(普通) * @param opt * @param iconRect * @param textRect * @param indicatorArrowRect * @param spacing * @param indicatorLen */ void SARibbonToolButton::PrivateData::calcLargeButtonDrawRects(const QStyleOptionToolButton& opt, QRect& iconRect, QRect& textRect, QRect& indicatorArrowRect, int spacing, int indicatorLen) const { //! 3行模式的图标比较大,文字换行情况下,indicator会动态调整 // 先获取文字矩形的高度 int textHeight = calcTextDrawRectHeight(opt); bool hIndicator = hasIndicator(opt); if (!hIndicator) { // 没有菜单,把len设置为0 indicatorLen = 0; indicatorArrowRect = QRect(); } // 这里要判断文字是否要换行显示,换行显示的文字的indicatorArrowRect所处的位置不一样 // 先布置textRect if (isEnableWordWrap()) { // 在换行模式下 if (isTextNeedWrap()) { // 如果文字的确换行,indicator放在最右边 textRect = QRect(spacing, opt.rect.bottom() - spacing - textHeight, opt.rect.width() - 2 * spacing - indicatorLen, textHeight); if (hIndicator) { indicatorArrowRect = QRect(textRect.right(), textRect.y() + textRect.height() / 2, indicatorLen, textHeight / 2); } } else { // 如果文字不需要换行,indicator在下板行 textRect = QRect(spacing, opt.rect.bottom() - spacing - textHeight, opt.rect.width() - 2 * spacing, textHeight); if (hIndicator) { int dy = textRect.height() / 2; dy += (dy - indicatorLen) / 2; indicatorArrowRect = QRect(textRect.x(), textRect.y() + dy, textRect.width(), indicatorLen); } } } else { // 文字不换行,indicator放在最右边 int y = opt.rect.bottom() - spacing - textHeight; if (hIndicator) { // 先布置indicator indicatorArrowRect = QRect(opt.rect.right() - indicatorLen - spacing, y, indicatorLen, textHeight); textRect = QRect(spacing, y, indicatorArrowRect.x() - spacing, textHeight); } else { textRect = QRect(spacing, y, opt.rect.width() - 2 * spacing, textHeight); } } // 剩下就是icon区域 iconRect = QRect(spacing, spacing, opt.rect.width() - 2 * spacing, textRect.top() - 2 * spacing); } /** * @brief 适应iconsize * @param buttonRect * @param originIconSize * @return */ QSize SARibbonToolButton::PrivateData::adjustIconSize(const QRect& buttonRect, const QSize& originIconSize) const { QSize iconSize = originIconSize; if (iconSize.height() > buttonRect.height()) { // 说明图标的icon过大,要匹配到buttonRect iconSize.setHeight(buttonRect.height()); // 等比例设置宽度 iconSize.setWidth(originIconSize.width() * iconSize.height() / originIconSize.height()); } return iconSize; } /** * @brief 判断是否有Indicator * @param opt * @return */ bool SARibbonToolButton::PrivateData::hasIndicator(const QStyleOptionToolButton& opt) const { return ((opt.features & QStyleOptionToolButton::MenuButtonPopup) || (opt.features & QStyleOptionToolButton::HasMenu)); } /** * @brief 计算sizehint * * 此函数非常关键,因为所有尺寸计算都是基于原始的rect来的 * @param opt * @return */ QSize SARibbonToolButton::PrivateData::calcSizeHint(const QStyleOptionToolButton& opt) { if (SARibbonToolButton::LargeButton == mButtonType) { return calcLargeButtonSizeHint(opt); } return calcSmallButtonSizeHint(opt); } QSize SARibbonToolButton::PrivateData::calcSmallButtonSizeHint(const QStyleOptionToolButton& opt) { int w = 0, h = 0; switch (opt.toolButtonStyle) { case Qt::ToolButtonIconOnly: { w = opt.iconSize.width() + 2 * mSpacing; h = opt.iconSize.height() + 2 * mSpacing; } break; case Qt::ToolButtonTextOnly: { QSize textSize = opt.fontMetrics.size(Qt::TextShowMnemonic, simplified(opt.text)); textSize.setWidth(textSize.width() + SA_FONTMETRICS_WIDTH(opt.fontMetrics, (QLatin1Char(' '))) * 2); textSize.setHeight(calcTextDrawRectHeight(opt)); w = textSize.width() + 2 * mSpacing; h = textSize.height() + 2 * mSpacing; } break; default: { // 先加入icon的尺寸 w = opt.iconSize.width() + 2 * mSpacing; h = opt.iconSize.height() + 2 * mSpacing; // 再加入文本的长度 if (!opt.text.isEmpty()) { QSize textSize = opt.fontMetrics.size(Qt::TextShowMnemonic, simplified(opt.text)); textSize.setWidth(textSize.width() + SA_FONTMETRICS_WIDTH(opt.fontMetrics, (QLatin1Char(' '))) * 2); textSize.setHeight(calcTextDrawRectHeight(opt)); w += mSpacing; w += textSize.width(); h = qMax(h, (textSize.height() + (2 * mSpacing))); } else { // 没有文本的时候也要设置一下高度 QSize textSize = opt.fontMetrics.size(Qt::TextShowMnemonic, " "); h = qMax(h, (textSize.height() + (2 * mSpacing))); } } } if (hasIndicator(opt)) { // 存在indicator的按钮,宽度尺寸要扩展 w += mIndicatorLen; } if (w < 16) { w = 16; } //! Qt6.4 取消了QApplication::globalStrut return QSize(w, h).expandedTo(QSize(2, 2)); } QSize SARibbonToolButton::PrivateData::calcLargeButtonSizeHint(const QStyleOptionToolButton& opt) { int w = 0; int h = opt.fontMetrics.lineSpacing() * 4.8; // 3*1.6 int minW = h * 0.75; // 最小宽度,在pannel里面的按钮,最小宽度要和icon适应 if (SARibbonPannel* pannel = qobject_cast< SARibbonPannel* >(q_ptr->parent())) { // 对于建立在SARibbonPannel的基础上的大按钮,把高度设置为SARibbonPannel计算的大按钮高度 h = pannel->largeButtonHeight(); } int textHeight = calcTextDrawRectHeight(opt); // 估算字体的宽度作为宽度 w = estimateLargeButtonTextWidth(h, textHeight, opt.text, opt.fontMetrics); w += (2 * mSpacing); // 判断是否需要加上indicator if (isEnableWordWrap() && isTextNeedWrap()) { w += mIndicatorLen; } #if SA_RIBBON_TOOLBUTTON_DEBUG_PRINT && SA_DEBUG_PRINT_SIZE_HINT qDebug() << "| | |-SARibbonToolButton::PrivateData::calcLargeButtonSizeHint,text=" << opt.text << "\n| | | |-lineSpacing*4.5=" << opt.fontMetrics.lineSpacing() * 4.5 // << "\n| | | |-textHeight=" << textHeight // << "\n| | | |-mDrawIconRect=" << mDrawIconRect // << "\n| | | |-minW=" << minW // << "\n| | | |-w=" << w // ; #endif //! Qt6.4 取消了QApplication::globalStrut return QSize(w, h).expandedTo(QSize(minW, textHeight)); } /** * @brief 计算文本高度 * @param opt * @return */ int SARibbonToolButton::PrivateData::calcTextDrawRectHeight(const QStyleOptionToolButton& opt) const { if (SARibbonToolButton::LargeButton == mButtonType) { if (isEnableWordWrap()) { return opt.fontMetrics.lineSpacing() * SARIBBONTOOLBUTTON_WORDWRAP_TEXT_FACTOR + opt.fontMetrics.leading(); } else { return opt.fontMetrics.lineSpacing() * SARIBBONTOOLBUTTON_NOWORDWRAP_TEXT_FACTOR; } } // 小按钮 return opt.fontMetrics.lineSpacing() * SARIBBONTOOLBUTTON_SMALLBUTTON_TEXT_FACTOR; } /** * @brief 估算一个最优的文字尺寸,在可以换行的情况下会进行换行,且只会换一行 * @param buttonHeight 按钮的高度 * @param textDrawRectHeight 文本绘制的高度 * @param fm QFontMetrics * @param widthHeightRatio 宽高比,宽度/高度的比值,如果大于这个比值,则会进行尝试换行以获取更低的宽度 * @param maxTrycount 尝试次数 * @return */ int SARibbonToolButton::PrivateData::estimateLargeButtonTextWidth(int buttonHeight, int textDrawRectHeight, const QString& text, const QFontMetrics& fm, float widthHeightRatio, int maxTrycount) { QSize textSize; int space = SA_FONTMETRICS_WIDTH(fm, (QLatin1Char(' '))) * 2; int hintMaxWidth = buttonHeight * widthHeightRatio; ///< 建议的宽度 if (isEnableWordWrap()) { textSize = fm.size(Qt::TextShowMnemonic, text); textSize.setWidth(textSize.width() + space); if (textSize.height() > fm.lineSpacing() * 1.1) { //! 说明文字带有换行符,是用户手动换行,这种情况就直接返回字体尺寸,不进行估算 mIsTextNeedWrap = true; // 文字需要换行显示,标记起来 return textSize.width(); } // 这时候需要估算文本的长度 if (textSize.width() <= hintMaxWidth) { // 范围合理,直接返回 mIsTextNeedWrap = false; // 文字不需要换行显示,标记起来 return textSize.width(); } //! 大于宽高比尝试进行文字换行 //! 这里先对文本长度逐渐加长估算,一直到和原来长度一致为止 int trycount = 0; int alignment = Qt::TextShowMnemonic | Qt::TextWordWrap; // 对于英文字体,直接宽度减半是无法满足完全显示两行的,需要进行预估 QRect textRect(0, 0, textSize.width(), textDrawRectHeight); do { //! 先计算两行文本的紧凑矩形 //! 从一半开始逐渐递增 //! 第1次为 w/2 + w/2 * (0/3) //! 第2次为 w/2 + w/2 * (1/3) //! 第3次为 w/2 + w/2 * (2/3) textRect.setWidth(textSize.width() / 2 + (textSize.width() / 2) * (float(trycount) / maxTrycount)); textRect = fm.boundingRect(textRect, alignment, text); if (textRect.height() <= (fm.lineSpacing() * 2)) { // 保证在两行 mIsTextNeedWrap = true; // 文字需要换行显示,标记起来 return textRect.width(); } ++trycount; #if SARIBBONTOOLBUTTON_DEBUG_DRAW if (trycount > 1) { qDebug() << "estimateLargeButtonTextWidth,origin textSize=" << textSize << ",trycount=" << trycount << ",textRect=" << textRect; } #endif } while (trycount < 3); // 到这里说明前面的尝试失败,最终使用原始的长度 return textSize.width(); } //! 说明是不换行 mIsTextNeedWrap = false; // 文字不需要换行显示,标记起来 // 文字不换行情况下,做simplified处理 textSize = fm.size(Qt::TextShowMnemonic, simplified(text)); textSize.setWidth(textSize.width() + space); if (textSize.width() < hintMaxWidth) { // 范围合理,直接返回 return textSize.width(); } if (textSize.width() > q_ptr->maximumWidth()) { // 超出了极限,就返回极限 return q_ptr->maximumWidth(); } return hintMaxWidth; } QPixmap SARibbonToolButton::PrivateData::createIconPixmap(const QStyleOptionToolButton& opt, const QSize& iconsize) const { if (opt.icon.isNull()) { // 没有有图标 return (QPixmap()); } QIcon::State state = (opt.state & QStyle::State_On) ? QIcon::On : QIcon::Off; QIcon::Mode mode; if (!(opt.state & QStyle::State_Enabled)) { mode = QIcon::Disabled; } else if ((opt.state & QStyle::State_MouseOver) && (opt.state & QStyle::State_AutoRaise)) { mode = QIcon::Active; } else { mode = QIcon::Normal; } // 添加高分屏支持 QSize pxiampSize = iconsize - QSize(2, 2); return opt.icon.pixmap(pxiampSize, mode, state); } int SARibbonToolButton::PrivateData::getTextAlignment() const { int alignment = Qt::TextShowMnemonic; if (SARibbonToolButton::LargeButton == mButtonType) { if (isEnableWordWrap()) { alignment |= Qt::TextWordWrap | Qt::AlignTop | Qt::AlignHCenter; // 换行的情况下,顶部对齐 } else { alignment |= Qt::AlignCenter; } } else { alignment |= Qt::AlignCenter; } return alignment; } /** * @brief 确认文字是否确切要换行显示 * @return */ bool SARibbonToolButton::PrivateData::isTextNeedWrap() const { return mIsTextNeedWrap; } /** * @brief 仅仅对\n进行剔除 * @param str * @return */ QString SARibbonToolButton::PrivateData::simplified(const QString& str) { QString res = str; res.remove('\n'); return res; } //=================================================== // SARibbonToolButton //=================================================== SARibbonToolButton::SARibbonToolButton(QWidget* parent) : QToolButton(parent), d_ptr(new SARibbonToolButton::PrivateData(this)) { setAutoRaise(true); setToolButtonStyle(Qt::ToolButtonTextBesideIcon); setButtonType(SmallButton); setMouseTracking(true); } SARibbonToolButton::SARibbonToolButton(QAction* defaultAction, QWidget* parent) : QToolButton(parent), d_ptr(new SARibbonToolButton::PrivateData(this)) { setAutoRaise(true); setToolButtonStyle(Qt::ToolButtonTextBesideIcon); setDefaultAction(defaultAction); setButtonType(SmallButton); setMouseTracking(true); } SARibbonToolButton::~SARibbonToolButton() { } /** * @brief 鼠标移动事件 * * 由于Ribbon的Indicator和正常的Toolbutton不一样,因此无法用QStyleOptionToolButton的activeSubControls的状态 * * 因此需要重新捕获鼠标的位置来更新按钮当前的一些状态 * @param e */ void SARibbonToolButton::mouseMoveEvent(QMouseEvent* e) { d_ptr->updateStatusByMousePosition(e->pos()); QToolButton::mouseMoveEvent(e); } /** * @brief SARibbonToolButton::mousePressEvent * @param e */ void SARibbonToolButton::mousePressEvent(QMouseEvent* e) { if ((e->button() == Qt::LeftButton) && (popupMode() == MenuButtonPopup)) { d_ptr->updateStatusByMousePosition(e->pos()); if (d_ptr->mMouseOnSubControl) { d_ptr->mMenuButtonPressed = true; showMenu(); // showmenu结束后,在判断当前的鼠标位置是否是在subcontrol d_ptr->updateStatusByMousePosition(mapFromGlobal(QCursor::pos())); return; } } d_ptr->mMenuButtonPressed = false; //! 注意这里要用QAbstractButton的mousePressEvent,而不是QToolButton的mousePressEvent //! QToolButton的mousePressEvent主要是为了弹出菜单,这里弹出菜单的方式是不一样的,因此不能执行QToolButton的mousePressEvent QAbstractButton::mousePressEvent(e); } void SARibbonToolButton::mouseReleaseEvent(QMouseEvent* e) { d_ptr->mMenuButtonPressed = false; QToolButton::mouseReleaseEvent(e); } void SARibbonToolButton::focusOutEvent(QFocusEvent* e) { d_ptr->mMouseOnSubControl = false; QToolButton::focusOutEvent(e); } void SARibbonToolButton::leaveEvent(QEvent* e) { d_ptr->mMouseOnSubControl = false; QToolButton::leaveEvent(e); } bool SARibbonToolButton::hitButton(const QPoint& pos) const { if (QToolButton::hitButton(pos)) { return (!d_ptr->mMenuButtonPressed); } return (false); } /** * @brief 在resizeevent计算绘图所需的尺寸,避免在绘图过程中实时绘制提高效率 * @param e */ void SARibbonToolButton::resizeEvent(QResizeEvent* e) { // 在resizeevent计算绘图所需的尺寸,避免在绘图过程中实时绘制提高效率 QToolButton::resizeEvent(e); updateRect(); } /** * @brief toolbutton的尺寸确定是先定下字体的尺寸,再定下icon的尺寸,自底向上,保证字体能显示两行 * @note m_sizeHint的刷新需要注意 * @return */ QSize SARibbonToolButton::sizeHint() const { // if (!d_ptr->mSizeHint.isValid()) { // 22是给与sizehint的最小值,如果小于这个值,重新计算一下 // QStyleOptionToolButton opt; // initStyleOption(&opt); // d_ptr->updateSizeHint(opt); // } #if SA_RIBBON_TOOLBUTTON_DEBUG_PRINT && SA_DEBUG_PRINT_SIZE_HINT qDebug() << "| | |-SARibbonToolButton::sizeHint"; #endif QStyleOptionToolButton opt; initStyleOption(&opt); d_ptr->updateSizeHint(opt); return d_ptr->mSizeHint; } void SARibbonToolButton::paintEvent(QPaintEvent* e) { Q_UNUSED(e); QPainter p(this); QStyleOptionToolButton opt; initStyleOption(&opt); if (opt.features & QStyleOptionToolButton::MenuButtonPopup || opt.features & QStyleOptionToolButton::HasMenu) { // 在菜单弹出消失后,需要通过此方法取消掉鼠标停留 if (!rect().contains(mapFromGlobal(QCursor::pos()))) { opt.state &= ~QStyle::State_MouseOver; } } paintButton(p, opt); paintIcon(p, opt, d_ptr->mDrawIconRect); paintText(p, opt, d_ptr->mDrawTextRect); paintIndicator(p, opt, d_ptr->mDrawIndicatorArrowRect); } /** * @brief 绘制按钮 * @param p * @param opt */ void SARibbonToolButton::paintButton(QPainter& p, const QStyleOptionToolButton& opt) { // QStyle::State_Sunken 代表按钮按下去了 // QStyle::State_On 代表按钮按checked // QStyle::State_MouseOver 代表当前鼠标位于按钮上面 QStyleOption tool = opt; bool autoRaise = opt.state & QStyle::State_AutoRaise; // 绘制按钮 if (autoRaise) { // 这个是为了实现按钮点击下去后(QStyle::State_Sunken),能出现选中的状态 // 先绘制一个鼠标不在按钮上的状态 if (opt.state & QStyle::State_Sunken) { tool.state &= ~QStyle::State_MouseOver; } style()->drawPrimitive(QStyle::PE_PanelButtonTool, &tool, &p, this); } else { style()->drawPrimitive(QStyle::PE_PanelButtonBevel, &tool, &p, this); } // 针对MenuButtonPopup的ribbon样式的特殊绘制 if ((opt.subControls & QStyle::SC_ToolButton) && (opt.features & QStyleOptionToolButton::MenuButtonPopup)) { if (opt.state & QStyle::State_MouseOver) { // 鼠标在按钮上才进行绘制 if (!(opt.activeSubControls & QStyle::SC_ToolButtonMenu)) { // 按钮的菜单弹出时不做处理 if (LargeButton == d_ptr->mButtonType) { // 大按钮模式 if (d_ptr->mMouseOnSubControl) { // 此时鼠标在indecater那 // 鼠标在文字区,把图标显示为正常(就是鼠标不放上去的状态) tool.rect = d_ptr->mDrawIconRect; tool.state |= (QStyle::State_Raised); // 把图标区域显示为正常 tool.state &= ~QStyle::State_MouseOver; if (autoRaise) { style()->drawPrimitive(QStyle::PE_PanelButtonTool, &tool, &p, this); } else { style()->drawPrimitive(QStyle::PE_PanelButtonBevel, &tool, &p, this); } } else { // 鼠标在图标区,把文字显示为正常 if (!tool.state.testFlag(QStyle::State_Sunken)) { // State_Sunken说明此按钮正在按下,这时候,文本区域不需要绘制,只有在非按下状态才需要绘制 tool.state |= (QStyle::State_Raised); // 把图标区域显示为正常 tool.state &= ~QStyle::State_MouseOver; // 文字和Indicator都显示正常 tool.rect = d_ptr->mDrawTextRect.united(d_ptr->mDrawIndicatorArrowRect); if (autoRaise) { style()->drawPrimitive(QStyle::PE_PanelButtonTool, &tool, &p, this); } else { style()->drawPrimitive(QStyle::PE_PanelButtonBevel, &tool, &p, this); } } } } else { // 小按钮模式 if (d_ptr->mMouseOnSubControl) { // 此时鼠标在indecater那 // 鼠标在文字区,把图标和文字显示为正常 tool.rect = d_ptr->mDrawIconRect.united(d_ptr->mDrawTextRect); tool.state = (QStyle::State_Raised); // 把图标区域显示为正常 if (autoRaise) { style()->drawPrimitive(QStyle::PE_PanelButtonTool, &tool, &p, this); } else { style()->drawPrimitive(QStyle::PE_PanelButtonBevel, &tool, &p, this); } } else { // 鼠标在图标区,把文字显示为正常 tool.state = (QStyle::State_Raised); // 把图标区域显示为正常 // 文字和Indicator都显示正常 tool.rect = d_ptr->mDrawIndicatorArrowRect; if (autoRaise) { style()->drawPrimitive(QStyle::PE_PanelButtonTool, &tool, &p, this); } else { style()->drawPrimitive(QStyle::PE_PanelButtonBevel, &tool, &p, this); } } } } } } // 绘制Focus // if (opt.state & QStyle::State_HasFocus) { // QStyleOptionFocusRect fr; // fr.QStyleOption::operator=(opt); // fr.rect.adjust(d_ptr->mSpacing, d_ptr->mSpacing, -d_ptr->mSpacing, -d_ptr->mSpacing); // style()->drawPrimitive(QStyle::PE_FrameFocusRect, &fr, &p, this); // } } /** * @brief 绘制图标 * @param p * @param opt */ void SARibbonToolButton::paintIcon(QPainter& p, const QStyleOptionToolButton& opt, const QRect& iconDrawRect) { if (!iconDrawRect.isValid()) { return; } QPixmap pm = d_ptr->createIconPixmap(opt, iconDrawRect.size()); style()->drawItemPixmap(&p, iconDrawRect, Qt::AlignCenter, pm); SARIBBONTOOLBUTTON_DEBUG_DRAW_RECT(p, iconDrawRect); } /** * @brief 绘制文本 * @param p * @param opt */ void SARibbonToolButton::paintText(QPainter& p, const QStyleOptionToolButton& opt, const QRect& textDrawRect) { int alignment = d_ptr->getTextAlignment(); if (!style()->styleHint(QStyle::SH_UnderlineShortcut, &opt, this)) { alignment |= Qt::TextHideMnemonic; } QString text; if (isSmallRibbonButton()) { text = opt.fontMetrics.elidedText(PrivateData::simplified(opt.text), Qt::ElideRight, textDrawRect.width(), alignment); } else { if (!isEnableWordWrap()) { text = opt.fontMetrics.elidedText(PrivateData::simplified(opt.text), Qt::ElideRight, textDrawRect.width(), alignment); } else { text = opt.text; } } //! 以下内容参考QCommonStyle.cpp //! void QCommonStyle::drawComplexControl(ComplexControl cc, const QStyleOptionComplex *opt,QPainter *p, const QWidget *widget) const //! case CC_ToolButton: QStyle::State bflags = opt.state & ~QStyle::State_Sunken; if (bflags & QStyle::State_AutoRaise) { if (!(bflags & QStyle::State_MouseOver) || !(bflags & QStyle::State_Enabled)) { bflags &= ~QStyle::State_Raised; } } if (opt.state & QStyle::State_Sunken) { if (opt.activeSubControls & QStyle::SC_ToolButton) { bflags |= QStyle::State_Sunken; } } QStyleOptionToolButton label = opt; label.state = bflags; style()->drawItemText(&p, textDrawRect, alignment, label.palette, label.state & QStyle::State_Enabled, text, QPalette::ButtonText); SARIBBONTOOLBUTTON_DEBUG_DRAW_RECT(p, textDrawRect); } /** * @brief 绘制Indicator * @param p * @param opt */ void SARibbonToolButton::paintIndicator(QPainter& p, const QStyleOptionToolButton& opt, const QRect& indicatorDrawRect) { if (!indicatorDrawRect.isValid() || !d_ptr->hasIndicator(opt)) { return; } QStyleOption tool = opt; tool.rect = indicatorDrawRect; style()->drawPrimitive(QStyle::PE_IndicatorArrowDown, &tool, &p, this); SARIBBONTOOLBUTTON_DEBUG_DRAW_RECT(p, indicatorDrawRect); } void SARibbonToolButton::drawArrow(const QStyle* style, const QStyleOptionToolButton* toolbutton, const QRect& rect, QPainter* painter, const QWidget* widget) { QStyle::PrimitiveElement pe; switch (toolbutton->arrowType) { case Qt::LeftArrow: pe = QStyle::PE_IndicatorArrowLeft; break; case Qt::RightArrow: pe = QStyle::PE_IndicatorArrowRight; break; case Qt::UpArrow: pe = QStyle::PE_IndicatorArrowUp; break; case Qt::DownArrow: pe = QStyle::PE_IndicatorArrowDown; break; default: return; } QStyleOption arrowOpt = *toolbutton; arrowOpt.rect = rect; style->drawPrimitive(pe, &arrowOpt, painter, widget); } void SARibbonToolButton::actionEvent(QActionEvent* e) { QToolButton::actionEvent(e); updateRect(); } /** * @brief 按钮样式 * @sa setButtonType * @return */ SARibbonToolButton::RibbonButtonType SARibbonToolButton::buttonType() const { return (d_ptr->mButtonType); } /** * @brief 设置按钮样式 * @note 设置按钮样式过程会调用setToolButtonStyle,如果要改变toolButtonStyle,如设置为Qt::ToolButtonIconOnly,需要在此函数之后设置 * @param buttonType */ void SARibbonToolButton::setButtonType(const RibbonButtonType& buttonType) { d_ptr->mButtonType = buttonType; // 计算iconrect // 根据字体计算文字的高度 if (LargeButton == buttonType) { setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Expanding); } else { setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum); } updateRect(); } /** * @brief 是否是小按钮 * @return */ bool SARibbonToolButton::isSmallRibbonButton() const { return (d_ptr->mButtonType == SmallButton); } /** * @brief 是否是大按钮 * @return */ bool SARibbonToolButton::isLargeRibbonButton() const { return (d_ptr->mButtonType == LargeButton); } QSize SARibbonToolButton::minimumSizeHint() const { return (sizeHint()); } /** * @brief 间距是几个重要矩形的间隔 * @return */ int SARibbonToolButton::spacing() const { return d_ptr->mSpacing; } void SARibbonToolButton::updateRect() { QStyleOptionToolButton opt; initStyleOption(&opt); d_ptr->updateDrawRect(opt); } /** * @brief 设置在lite模式下是否允许文字换行,如果允许,则图标相对比较小,默认不允许 * @param on */ void SARibbonToolButton::setEnableWordWrap(bool on) { SARibbonToolButton::PrivateData::s_enableWordWrap = on; } /** * @brief 在lite模式下是否允许文字换行 * @return */ bool SARibbonToolButton::isEnableWordWrap() { return SARibbonToolButton::PrivateData::s_enableWordWrap; } bool SARibbonToolButton::event(QEvent* e) { switch (e->type()) { case QEvent::WindowDeactivate: d_ptr->mMouseOnSubControl = false; break; case QEvent::ActionChanged: case QEvent::ActionRemoved: case QEvent::ActionAdded: { d_ptr->mMouseOnSubControl = false; updateRect(); } break; default: break; } return (QToolButton::event(e)); } void SARibbonToolButton::changeEvent(QEvent* e) { if (e) { if (e->type() == QEvent::FontChange) { // 说明字体改变,需要重新计算和字体相关的信息 updateRect(); } } QToolButton::changeEvent(e); }