Qt/GUI美化/样式表/QToolBox使用

概述

起因,刚开始用这个类,一直想整理,却没有很大的动力。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的内存一直安稳的存在着,但是确实出问题了…

从异常开始

  1. 使用Qt Designer绘制Tab Widget、Stacked Widget等容器控件时,通过右键菜单都可以删除全部的Page,独独Tool Box不可以,其最后一项不允许被删除。
  2. 每个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可以做

  1. 使用QToolBox可以实现类似于QQ分组的效果
    在这里插入图片描述
  2. 使用QToolBox代替QTableWidget实现文件列表的分页展示。这是博主使用TBox的最初需求,实现效果还可以,人机体验有待观察。
  3. QToolBox经典使用案例
  4. 使用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")    -1TBItem(0xd9e3658, name = "item-object-1")
    QWidget(0xdbdf860, name = "qt_scrollarea_hcontainer")  -2QScrollBar(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;
}

其它,后记,方案优化:

  1. 上述代码是第二个版本,相比第一个版本增加了非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的对象的指针。
  1. 由于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操作。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值