文章目录
概述
起因,刚开始用这个类,一直想整理,却没有很大的动力。Today,亲爱的小瑶同学在使用QToolBox执行removeItem(all.)/addItem(pItem[0…])操作时,进程崩溃,还怀疑管帮中对renoveItem的描述(Removes the item at position index from the toolbox. Note that the widget is not deleted.)是不对的,非说Widget被删了。当然这种怀疑是站不住脚的,随便的运行调试,便可确定Widget的内存一直安稳的存在着,但是确实出问题了…
从异常开始
- 使用Qt Designer绘制Tab Widget、Stacked Widget等容器控件时,通过右键菜单都可以删除全部的Page,独独Tool Box不可以,其最后一项不允许被删除。
- 每个Widget对象,只能被add或insert一次,否则显示异常(被重复加入的这个widget所对应的Item,Widget显示show空)。(remove后可再次使用)
异常复现(0测)
class CTestWidget : public QWidget
{
private slots:
void on_pushButtonClear_clicked();
void on_pushButtonAdd_clicked();
void on_pushButtonTest_clicked();
private:
Ui::CTestWidget *ui;
CTBItem *m_pTBItem[2];
};
#endif // _RIVER_TESTWIDGET_H_
CTestWidget::CTestWidget(QWidget *parent) :
QWidget(parent),
ui(new Ui::CTestWidget)
{
ui->setupUi(this);
m_pTBItem[0] = new CTBItem();
m_pTBItem[1] = new CTBItem();
}
void CTestWidget::on_pushButtonClear_clicked()
{
ui->toolBox->removeItem(0);
ui->toolBox->removeItem(0);
}
void CTestWidget::on_pushButtonAdd_clicked()
{
ui->toolBox->addItem(m_pTBItem[0], "TOOLBOX-Item-1");
ui->toolBox->addItem(m_pTBItem[1], "TOOLBOX-Item-2");
}
//清除并新增(使用刚被删过的widget)
void CTestWidget::on_pushButtonTest_clicked()
{
ui->toolBox->removeItem(0);
ui->toolBox->removeItem(0);
ui->toolBox->addItem(m_pTBItem[0], "TOOLBOX-Item-x");
}
依次执行,清空(designer绘制项)、新增两项(自定义Item)、清除并新增,将会出现如下崩溃提示:
追踪堆栈信息(Qt源码进过本地编译):
据上述堆栈信息,博主的水平只能推断出,是挂在事件处理啦,且与布局更新有关。
附加1测
不使用QtDesigner绘制ToolBox,而是手动new出一个。(相比绘制,其初始状态不必,至少已存在一个Page项)。测试结果与0测一致。
附加2测
void CTestWidget::on_pushButtonAdd_clicked()
{
ui->toolBox->addItem(m_pTBItem[0], "TOOLBOX-Item-1");
//解封这行测试效果不变
//ui->toolBox->addItem(m_pTBItem[0], "TOOLBOX-Item-2");
}
//清除并新增(使用刚被删过的widget)
void CTestWidget::on_pushButtonTest_clicked()
{
ui->toolBox->removeItem(0);
ui->toolBox->addItem(m_pTBItem[0], "TOOLBOX-Item-x");
}
执行过程同上,不会出现异常。(当ToolBox仅有一个page时,清除它并立即新加入它)
附加3测
//.h
CTBItem *m_pTBItem[3];
//cpp
void CTestWidget::on_pushButtonTest_clicked()
{
ui->toolBox->removeItem(0);
ui->toolBox->removeItem(0);
//增加了一个不是刚刚remove的widget项
ui->toolBox->addItem(m_pTBItem[2], "TOOLBOX-Item-x");
}
执行过程同上,不会出现异常。
临时解决办法
-
方案1
当Item大于2时,将清除操作与增加(复用被删widget)操作,分散到两个槽函数中执行。 -
方案2
执行清空操作后,不要复用刚被remove的widget,而是固定的add数组之外的另一个widgetAdd,这样便不会崩溃。(如:m_pTBItem[2],widgetAdd、widgetCopy - 相当于每次ToolBox页面使用一类widget,她们互不交涉)
Index使用异常
Signals
void currentChanged(int index)
在QToolBox的派生类中写了一个槽(其中使用到widget(index)函数),链接到上述信号,在迭代测试过程中,特殊情况下此处无规律崩溃。经过调试追踪,发现崩溃时,index的值是-1使得widget(index)为空指针。怀疑aToolBox中项被清空时,也向外部发送了一个currentChanged信号。(经简例验证,TBox在removeItem掉最后一项时,会发送currentChanged(-1))。
临时解决办法:
在槽函数中判断index为-1时,执行return操作。其他项视图也会存在index变-1的响应,只是我们从未那么操作过。
延伸思考:
项视图的清空项操作会使得index变为-1并自动的发送此信号,这也可能是导致开篇操作运行的原因所在。
QToolBox可以做
- 使用QToolBox可以实现类似于QQ分组的效果
- 使用QToolBox代替QTableWidget实现文件列表的分页展示。这是博主使用TBox的最初需求,实现效果还可以,人机体验有待观察。
- QToolBox经典使用案例
- 使用QToolBox可以很容易的实现折叠/展开面板的功能。具体可以参见《Qt/GUI/布局/实现窗口折叠效果》文章。
外围控件失能管理
需求来源:当TBox的某个Item-Widget内的操作未确定完成时,介于数据编辑操作顺序,很可能不希望执行该Widget之外的其他操作。
/** @brief 2019-08-20 晚 //该函数最终在App派生类的notify()函数中调用 **/
bool CEnableAreaMgr::DealDisableEvent(QObject *pObj, QEvent *pEvent)
{
if (pEvent->type() == QEvent::MouseButtonPress)
{
if (!IsEnableOperate((QMouseEvent*)pEvent, pObj))
{
//提取控件 //也可直接传进pObj对象指针
//m_pDisableWidget = QApplication::widgetAt(((QMouseEvent*)pEvent)->globalPos());
m_pDisableWidget = (QWidget*)pObj; //被标记当前禁止操作的控件
m_PaletteLast = m_pDisableWidget->palette();
m_pDisableWidget->setAutoFillBackground(true);
m_pDisableWidget->setPalette(m_PaletteWarn);
return true; //事件处理完成
}
else { m_pDisableWidget = NULL; }
}
else if (pEvent->type() == QEvent::MouseButtonRelease)
{
if (NULL != m_pDisableWidget)
{
m_pDisableWidget->setPalette(m_PaletteLast);
return true;
}
}
else if (pEvent->type() == QEvent::KeyPress)
{ //对于按钮事件 如Key_Escape可以做类似处理 }
else if (pEvent->type() == QEvent::KeyRelease)
{ //对于按钮事件 如Key_Escape可以做类似处理 }
return false; //走默认处理
}
//2019-08-20 晚 //判断点击位置是否在允许范围内
bool CEnableAreaMgr::IsEnableOperate(QMouseEvent *pEvent, QObject *pObj)
{
if (m_lstEnWidgets.isEmpty())
return true; //相当于没有开启
foreach (QWidget* pWidget, m_lstEnWidgets)
{
QRect rect = pWidget->rect();
QPoint pointNew = pWidget->mapToGlobal(/*rect.topLeft()*/QPoint(0, 0)); //在show之前调用该函数无效
QRect rectGlobal = QRect(pointNew, rect.size());
if (rectGlobal.contains(pEvent->globalPos()))
return true;
}
/** @brief QToolButton / QPushButton etc.. /自定义窗口类名称 **/
if (m_lstLimitWarnType.contains(QString(pObj->metaObject()->className())))
{
qDebug() << "[Warning] this widget is out of area that can been operated!";
return false; //指定区域外的操作
}
return true;
}
QToolBox的组成
//在某ToolBox对象中增加两个Item项,测试结果如下:
//遍历各级对象的children 执行如下打印
//qDebug() << pobj << pobj->children().size(); //伪代码
m_pToolBox->children() 主要打印如下 //第一级子对象,及其各自的再下一级子对象的个数
QVBoxLayout(0x2ce28) - 0
QToolBoxButton(0xdac3710, name = "qt_toolbox_toolboxbutton") - 0, QScrollArea(0xdade908) - 3
QToolBoxButton(0xdad2e08, name = "qt_toolbox_toolboxbutton") - 0, QScrollArea(0xdacfb78) - 3
Part-QScrollArea has Children: //亦是ScrollArea的固有组成
QWidget(0xdbdeda0, name = "qt_scrollarea_viewport") -1 : TBItem(0xd9e3658, name = "item-object-1")
QWidget(0xdbdf860, name = "qt_scrollarea_hcontainer") -2 : QScrollBar(0xda105c8) , QBoxLayout(0xda10948)
QWidget(0xdbfdc60, name = "qt_scrollarea_vcontainer") -2 : QScrollBar(0xd9e6cc8) , QBoxLayout(0xd9e7048)
- 结论,QToolBox类对客户开放 obj_page(QWidget),其层级关系: QToolBox -> QScrollArea_Object -> qt_scrollarea_viewport -> QWidget_Item;且,在insertItem接口中QToolBoxButton、QScrollArea将会被重新建立,对应的removeItem中它们被delete掉,涉及代码如下:
//QToolBox::insertItem
c.button = new QToolBoxButton(this);
c.button->setObjectName(QLatin1String("qt_toolbox_toolboxbutton"));
c.sv = new QScrollArea(this);
c.sv->setWidget(widget);
//QToolBox::removeItem _q_widgetDestroyed 函数执行
layout->removeWidget(c->sv);
layout->removeWidget(c->button);
c->sv->deleteLater(); // page might still be a child of sv
delete c->button;
关于"抢焦点Focus"
场景,博主使用的嵌入式移动设备同时支持触摸屏和部分物理按键的操作,而在Qt的事件处理系统中,物理按键事件的捕获是依托于当前焦点控件的(焦点在哪个控件,物理按键事件则将由它捕获);对这种场景的通常处理方案是:在window窗口对象实现过滤器,将自身及其可得焦点的子部件全部安装,这样无论焦点在哪个控件上,当收到Qt::Key_A/Qt::Key_Enter/Qt::Key_D…按键事件时,均能做相应处理。问题来了,在window窗口中使用了QToolBox控件(FocusePolicy::NoFocus)后,点击过tooBox控件对象后,过滤器中收不到事件了,通过CMyApplication::notify(…) 打印追踪,发现,这时候的事件接收者是 QScrollArea 的某个不知名对象。
其实,阅读过 < QToolBox的组成 > 这一节内容后,基本可以理解上述问题的成因啦;进一步测试如下:打开ui编辑器,拖一个QScrollArea控件进去,查看其focusPolicy属性为-StrongFocus,至此问题的原因就清晰了。解决办法为:
//plan-1
QObjectList lstObj = ui->toolBoxProPtList->children();
foreach (QObject *pObj, lstObj) {
if ("QScrollArea" == QString(pObj->metaObject()->className()))
{
((QScrollArea*)pObj)->setFocusPolicy(Qt::NoFocus);
}
}
//plan-2 //invoke plan-1 when itermchanged everytime
//plan-3
virtual void itemInserted(int index)
{
if (-1 == index)
return;
//get ScrollArea object and set it's FocusPolicy
widget(index)->parentWidget()->parentWidget()->setFocusPolicy(Qt::NoFocus);
}
但是,此处还有一个坑,这点也能从 < QToolBox的组成 > 发现原因,toolBox的ScrollArea对象并不是固定的,它随着item的insert/remove操作在new/delete; 所以,在自定义toolBox对象的构造中执行上一段setFocusPolicy可能会在操作后失效,其必须在每次Hmi刷新后重新执行上述代码,或者在 virtual itemInserted 函数中执行对应ScrollArea的FocusPolicy设置。
PS:焦点的传递
-
进一步测试分析可得到:由QToolBox内部创建的私有的toolBoxButton是Policy::NoFocus属的。只有当点击过toolBox的Widget(NoFocus),才会导致焦点被widget的祖父窗口ScrollArea_object得到。
-
通用测试结论:当我们点击了一个NoFocus属性的window子部件后,Qt框架沿着该控件的父关系逐层向上查找(不会向下),直到找到一个可获取焦点的父窗控件,并将设置为App的FocusWidget,若找不到,则维持上一个FocusWidget不变。(A widget that is not embedded in a parent widget is called a window. [-QWidget Detailed Description])
-
使用Tab进行焦点切换时,以一个window为界限,循环遍历其中的所有焦点控件。
样式效果优化
基本效果
//如何设置项的高度
setStyleSheet("QToolBoxButton { min-height:50px; }");
//如何设置项之间的间距
this->layout()->setSpacing(x);
//需要注意的是 其必须要在项添加后再执行 才可以生效
//设置ToolBox/page /选中page的颜色
ui->toolBox->setStyleSheet("QToolBox::tab:selected {color:blue;}");
附加信息对齐
如前边的QQ分组图中,显示在线好友个数的 4/11…附加信息,上图红色方框标记。
通过源码分析,QToolBoxButton是私有类(Inherits from QAbstractButton),不对客户开放。通过QToolBox接口文档来看,导出(public & protected)接口不能将ToolBoxBtn与widget关联起来,故通过index去操作对应的toolBtn对象可行性较低,非要硬凑的话只能去取控件位置来解析啦,不合适。那么,能否在父窗(toolBox)中能否实现子窗(toolBoxBtn)的重绘呢?
在父窗的过滤器中,是可以实现的,有可行方案如:子对象安装父窗所持事件过滤器,等到子窗的Paint事件时,拦截处理并结束该paint事件。但是,意图在父窗的paintEvent实现该功能不可取,只会牺牲自己却也不能成就别人,因其只捕获自身paint事件,即使强硬的绘制它的子窗,也阻挡不了接下来子窗自己的固有绘制过程。Reason-PS:QWidget::paintEvent[Help Link], paint的顺序是由父窗到各级子窗,重写了父窗的paintEvent函数(即使实现为空),有时候看上去依然的显示正常,是因为各子窗口类固有的paintEvent过程依然在正常执行。
PS: 关于QPainter的两点未探明的问题:
- 进行setStyleSheet设置时,可能会影响到paint的过程。
- 定义QPainter的QPaintDevice时,若不与该Paint事件的持有者一致,可能会有如下告警 QPainter::xxfunc: Painter not active,过程无效。不管是否有意义,此处可能有其它手段能使之生效,如QPainter::begin调用等。
- 阅读QRect的帮助文档,一并纠正对包括width、height、padding、margin在内的概念理解。
//virtual //be invoked at func_QToolBox_insertItem's last
void CMyToolBox::itemInserted(int index)
{
Q_UNUSED(index);
/** @brief
* # the func is invoked in QToolBox::insertItem, also addItem
* # when installEventFilter(pOneObj) is repeat, the last pOneObj was supposed
* to be automatically removed.
* # plan 2: you can get QToolBoxButton obj by widget.rect() -> childAt(pos)
**/
QObjectList lstTbChildren = this->children();
foreach (QObject *pObj, lstTbChildren)
{
if ("QToolBoxButton" == QString(pObj->metaObject()->className()))
{
pObj->installEventFilter(this);
}
}
}
//virtual //be invoked at func_QToolBox_removeItem's last
void CMyToolBox::itemRemoved(int index)
{
/** @brief there is no need to exe pObjToolBoxButton->removeEventFilter(this), as for:
All event filters for this object are automatically removed when this object is destroyed.
**/
Q_UNUSED(index);
}
//copy from QToolBoxButton::initStyleOption //private
void CMyToolBox::initStyleOption(QStyleOptionToolBox *option, QAbstractButton *pbtnItem) const
{
if (!option)
return;
option->initFrom(pbtnItem);
if (/*selected*/ false)
option->state |= QStyle::State_Selected;
if (pbtnItem->isDown())
option->state |= QStyle::State_Sunken;
option->text = /*pbtnItem->text()*/""; //must enpty
option->icon = pbtnItem->icon();
if (QStyleOptionToolBoxV2 *optionV2 = qstyleoption_cast<QStyleOptionToolBoxV2 *>(option))
{
optionV2->position = QStyleOptionToolBoxV2::Middle;
}
}
bool CMyToolBox::eventFilter(QObject *pObj, QEvent *pEvent)
{
if (QEvent::Paint == pEvent->type())
{
if ("QToolBoxButton" == QString(pObj->metaObject()->className()))
{
QPainter painter((QWidget*)pObj); //Set QPaintDevice
//copy from QToolBoxButton::paintEvent
QPainter *p = &painter;
QStyleOptionToolBoxV2 opt;
initStyleOption(&opt, (QAbstractButton*)pObj);
((QWidget*)pObj)->style()->drawControl(QStyle::CE_ToolBoxTab, &opt, p, parentWidget());
QString strText = ((QAbstractButton*)pObj)->text();
QStringList lstStr = strText.split("{"); //spilt btn original Text Q_ASSERT(2 == lstStr.count());
QPen pen(Qt::blue); pen.setWidth(2); painter.setPen(pen);
QRect rect_btn = ((QWidget*)pObj)->rect();
QRect rect1 = QRect(rect_btn.topLeft().x() + 30, rect_btn.y(), rect_btn.width() - 130, rect_btn.height());
//redraw btnText part 1
painter.drawText(rect1, lstStr.at(0), QTextOption(Qt::AlignLeft|Qt::AlignVCenter));
QRect rect2 = QRect(rect1.topRight().x() - 100, rect1.y(), 100, rect1.height());
//redraw btnText part 2
painter.drawText(rect2, QString(lstStr.at(1)).remove("}"), QTextOption(Qt::AlignLeft|Qt::AlignVCenter));
return true;
}
}
return false;
}
其它,后记,方案优化:
- 上述代码是第二个版本,相比第一个版本增加了非text部分的绘制(移植自QToolBoxButton源码),基本维持了QToolBoxButton的原有绘制效果。但,版本2也存在缺陷,具体可参见QToolBoxButton::paintEvent源码,主要是丢失了关于QToolBoxButton私有变量indexInPage、selected导致的效果的变化。私有类中的私有变量,导出来是不可能的,我们只能在CMyToolBox类中自己实现他们的维护,可这有些强人所难,有如下思路(时间有限,未执行),如果觉得不可以容忍上述缺陷,则可尝试在CMyToolBox大约实现:
struct TTBoxBtnOtherInfo
{
bool selected;
int indexInPage;
}
QHash<QAbstractButton*, TTBoxBtnOtherInfo> m_TBoxBtnOtherInfo;
//在eventFilter中记录项的总个数countTotal,并维护m_TBoxBtnOtherInfo的赋值与清除操作(其个数始终为countTotal)。通过QTooBox的currentWidget窗口的显示位置,使用childAt(pos_cal),提取其对应的QToolBoxButton的对象的指针。
- 由于QToolBox控件的QToolBoxButton是完全私有的一个类,上述方案在实现上也略显复杂,所以萌生过,全移植QToolBox相关类实现的想法,简单测试后,该方案可行性较低。因,其中用到的QToolBoxPrivate私有类,其继承关系为QFramePrivate、QWidgetPrivate、QObjectPrivate有很长的延伸链,并不相对独立。
tab样式表
先来看看最纯正的QToolBox在windows和Linux上的样式效果差异:在windows上项视图的border是水平的,但是Linux上运行时,项视图的border在右侧末尾处会自动下折。开始搞不明白,是啥在作用,导致了上述不同;结果发现,对纯QToolBox对象执行下边最简单的样式设置,即可冲掉Linux原有效果,达到扩平台效果统一。
//发现根本不如原先的好看呢!
this->setStyleSheet("QToolBox::tab{border: 2px groove blue; border-radius:4px;}");
随将上述设置移植到上一节CMyToolBox中,执行却不生效,修改为 CMyToolBox::tab 也不生效。 后来通过知识扩充和测试,发现这种不生效是因为,自定义了QToolBoxButton的重绘过程导致的,至于再深层次的原因,未探究。
虽然tab的样式设置在QPainter双重作用下未生效,但是下边的样式代码却是可生效的。该问题留到样式表的相关博文中继续探讨。
//QToolBoxButton{min-height:40px}"); //可生效
//QToolBoxButton{background-color:lightblue} //可生效
通过如下,样式表简单设置,可以配置当前的QToolBoxButton展示的字体颜色。
//设置ToolBox/page /选中page的颜色
ui->toolBox->setStyleSheet("QToolBox::tab:selected {color:blue;}");
Qt-Designer绘制QToolBox
由于直接使用QtDesigner绘制QToolBox时,工具"意外的限制"至少要存在一个项(page-QWidget),这导致了嵌入式Linux的一个小运行效果异常:即使我们在初始化时,remove掉了这个page项,当在运行时加入自定义项(CMyWidget),显示效果如下:
右上脚阴影 | linux上项border下折效果 |
---|---|
其中,第一个项处的"阴影",正是那个在绘制时加入的widget项,尽管我们对其执行过remove,但是由于这个对象没有被销毁,它在Linux环境下显示了出来,一个最简单的解决办法是,在初始化时对这个page执行一次hide操作。