Qt: border-radius of page in QToolBox are broken —— QToolBox 中 page 的圆角边框无法闭合问题排查

1 异常现象

今天用到了 QToolBox,然后想给每个 page 添加圆角边框,于是使用了如下 stylesheet

QToolBox > QWidget   {
  border: 1px solid silver;
  border-radius: 6px;
}

却发现圆角边框无法闭合。

2 原因

然后查了半天,看了 Qt 源码。才发现原来我们直观以为的这个 page QWidget,并不是 QToolBox 的直接 child。而是 QToolBox > QScrollArea > QWidget ( QScrollArea.viewport ) > QWidget。

(注意,包括 QScrollArea、QTreeView 等这些控件内部的 viewport,并不是 QViewport 对象,而是一个隐藏的 QWidget 对象。QViewport 是 3D 显示用的。)

关键代码在 qtoolbox.cpp 中

这些控件的父子链如下图所示:

QToolBox 中的 QObject 父子链

我还写了一个测试代码,对 page 对象不断往上调用 parent(),得到其父子链结果如下:

3 解决办法

所以,我们在上面的 qss 中用的 QToolBox > QWidget 其实指向的是 QScrollArea,并不是我们要的 page。

而圆角边框无法闭合的问题,是 QScrollArea 固有的 BUG。我猜应该是跟 scrollbar 有关吧,先不管它了。

所以要解决 QToolBox 中 page 圆角边框的问题,只需要将我们的 qss 改成:

QToolBox > QScrollArea > QWidget > .QWidget   {
    border: 1px solid silver;
    border-radius: 6px;
}

4 其他问题

4.1 page 中的滚动条变样了

为什么上述解决办法中的选择器要用 QToolBox > QScrollArea > QWidget > .QWidget 而不是 QToolBox > QScrollArea > QWidget > QWidget

首先我们要知道最后一个 QWidget 前面加个点表示这项只匹配 QWidget 类型,不匹配 QWidget 的派生类。如果不加这个点,就是匹配 QWidget 以及其派生类。

然后,由于 QScrollArea 下面其实还有其他 QWidget 对象,hcontainer 与 vcontainer。这两个 container 中又包含滚动条 QScrollBar(派生自 QWidget)。所以如果使用 QScrollArea > QWidget > QWidget 来匹配的话,其样式会影响到滚动条的显示效果。

4.2 关于 tab 的高度

QToolBoxButton {
  min-height: 40px;
}

/* 这是无效的 */
QToolBox::tab {
  min-height: 40px;
}

因为 tab 的本质是一个 QToolBoxButton (继承自 QAbstractButton)。但为什么使用 ::tab 时候无效呢?我暂时也没去考证它的内部逻辑。

4.3 关于tab 与 page 之间的间隙

QToolBox::tab {
    background-color: #bbccdd;
}
QToolBox > QScrollArea > QWidget > .QWidget   {
    border: 1px solid silver;
    border-radius: 6px;
}

发现 tab 与 page 之间的间隙过大,想要调整。但这个用 qss 是无法实现的。

因为 QToolBox 的绘图盒模型是这样子的:

QToolBox 的盒模型(虚线表示不可见 widget 或 layout)

tab 与 page 以及 page 与下一个 tab 之间的间隙,其实是 QToolBox.layout().spacing 决定的。

5 更深入的问题!

5.1 QScrollArea 的圆角边框问题

使用 QToolBox > QScrollArea > QWidget > .QWidget 虽然找到了 page 本体,也给它设置了边框。但是由于这是 QScrollArea 内部控件的边框啊,所以当整个窗口被收缩,QScrollArea 出现滚动条后,这个 page 的上下边框可能就被滚动掉了!!!   这其实并不是我们的本意。

我们希望边框是一直都在的,不管你滚动条怎么滚动。所以其实,我们本来就应该对这个 QScrollArea 设置边框。

但 QScrollArea 的圆角边框无法正常显示圆角的现象,似乎就是一个 BUG。我找了一个相似问题(QTextArea 也是这样)的帖子 qt.io。并咨询了其中的大牛,得到的答复是:

Chris Kawa:

It’s a bit different issue. Setting border radius does not clip children painting to the rounded corners of the parent (I’m guessing for performance reasons).
Scroll area has a viewport widget and (optionally) scrollbars that stick to the edges of it, so they will always cover the parent’s rounded corners.

The easiest I think would be to disable the scroll area’s border entirely and place it in a container widget that has border with rounded corners and matching content margins set. This of course means that you would get a bit of empty space around the scroll area.

If that’s not an option you could maybe add a transparent for input widget on top of the scroll area (or 4 small ones in the corners) and draw the rounded corners in it. That should get you what you want but is a bit more work. Note that the rounded corner would cover scrollbars, which in some styles might not look too good.

为此我也特意测试了一下 QScrollArea 的父子链,得到的结果是:

QScrollArea 中的 QObject 父子链关系
QScrollArea 盒模型(虚线表示不可见 widget 或 layout)

另外,这两个 scrollbar 是否会在右下角重叠闭合,以及 scrollbar 是否会与 viewport 重叠,这得看全局的 QApplication.style。在 style(‘macOS’) 下,横竖滚动条之间以及滚动条与 viewport 是会重叠的。在 style(‘Windows’) 下,这三者是不会重叠的,右下角留空一个小正方形。

所以,如 Chris Kawa 所言,对于一个单独的 QScrollArea 来说,最好的办法是在它的外层包裹一个 QWidget,对这个 QWidget 设置 round border 就好了。

但对于我们眼前要做的 QToolBox 而言,QScrollArea 的外层以及是这个 QToolBox 了,而对 QToolBox 设置 border 并不是我们想要的效果(因为 QToolBox 里会有好多个 tab 与 QScrollBox)。

所以我还想到的一个折中办法。就是将 QToolBox 下的 QScrollArea 都设置 verticalScrollBar().setEnabled(False) 与 horizontalScrollBar().setEnabled(False)。然后在 page 中先铺垫一层 QScrollArea。再在这个 QScrollArea 中添加想要的控件。round border 可以照旧对 page 进行设置。相当于把 page 上层的 scrollarea 禁用,新建一个 scrollarea 放到 page 里面。这虽然麻烦,但也会有一些效果,可能并不完美。

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top