instAdmin/Cpp/GisDes/SaRibbon/SARibbonBar/SARibbonToolButton.cpp
2024-10-29 22:24:50 +08:00

1230 lines
45 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "SARibbonToolButton.h"
#include "SARibbonPannel.h"
#include <QAction>
#include <QApplication>
#include <QCursor>
#include <QMouseEvent>
#include <QPaintEvent>
#include <QStyleOption>
#include <QStyleOptionFocusRect>
#include <QStyleOptionToolButton>
#include <QStylePainter>
#include <QTextOption>
#include <QApplication>
#include <QScreen>
#include <QProxyStyle>
/**
* @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);
}