PyQt6 如何动态调整 QGroupBox 的大小以适应内容?

问题描述

我使用的是 Python 3.9.5 和 PyQt6。

根据我之前的问题,我想问一下如何动态调整 qgroupbox 的大小以适应其内容

我有一个 QScrollArea,它的布局是一个 QVBoxLayout,在 QVBoxLayout 中会添加一堆 qgroupbox

qgroupbox本身有一个QVBoxLayout,QVBoxLayout里面是一堆QVBoxLayout,qgroupbox的QVBoxLayout里面的QVBoxLayout里面就是内容

每个最低级别的 QVBoxLayout 都有两个小部件和一个 QHBoxLayout。

从上到下,第一个小部件是 QLabel,固定大小为 100x20,第二个小部件是 QTextEdit,可根据内容自动调整大小。而QHBoxLayout里面是一个伸缩的固定大小的按钮,大小为60x20,按钮可以隐藏也可以显示

小部件和布局的层次结构是这样的:

QVBoxLayout — level 0
    QScrollArea — level 1
        QVBoxLayout0 — level 2
            qgroupbox — level 3 (checkable)
                QVBoxLayout1 — level 4
                    QLabel — level 5 (fixed)
                    QTextEdit — level 5 (auto-resizing)
                    QHBoxLayout — level 5
                        Stretch — level 6
                        QPushButton — level 6 (fixed)

我希望 qgroupbox 能够自动调整大小以适合其内容

宽度由布局决定,我希望 qgroupboxs 有一个固定的、最小的、基于其内容的最佳高度。

基本上,高度应该是其所有 VISIBLE 小部件的高度之和,加上顶部边距和底部边距,以及小部件之间的边距,也许还有复选框的高度。

我想为 qgroupboxs 设置固定的高度,因为如果我不这样做,布局将拉伸 qgroupboxs 并将其放置在中间,而不是在顶部的最小高度,这不是我想要的。

我将在下面发布示例代码

from PyQt6.QtCore import *
from PyQt6.QtGui import *
from PyQt6.QtWidgets import *

font = QFont('Noto Serif',9)

def makebutton(text):
    button = QPushButton()
    button.setFont(font)
    button.setFixedSize(60,20)
    button.setText(text)
    return button

class Editor(QTextEdit):
    doubleClicked = pyqtSignal(QTextEdit)
    def __init__(self):
        super().__init__()
        self.setReadOnly(True)
        self.setFont(font)
        self.setFixedHeight(20)
        self.setAttribute(Qt.WidgetAttribute.WA_DontShowOnScreen)
        self.show()
        self.textChanged.connect(self.autoResize)
    
    def mouseDoubleClickEvent(self,e: QMouseEvent) -> None:
        self.doubleClicked.emit(self)
    
    def autoResize(self):
        self.document().setTextWidth(self.viewport().width())
        margins = self.contentsMargins()
        height = int(self.document().size().height() + margins.top() + margins.bottom())
        self.setFixedHeight(height)
    
    def resizeEvent(self,event):
        self.autoResize()


class textcell(QVBoxLayout):
    def __init__(self,text):
        super().__init__()
        self.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop)
        self.label = QLabel(text)
        self.label.setFixedSize(80,20)
        self.apply = makebutton('Apply')
        self.apply.hide()
        self.editor = Editor()
        self.editor.doubleClicked.connect(self.on_DoubleClick)
        self.hBox = QHBoxLayout()
        self.hBox.addStretch()
        self.hBox.addWidget(self.apply)
        self.addWidget(self.label)
        self.addWidget(self.editor)
        self.addLayout(self.hBox)
        self.apply.clicked.connect(self.on_ApplyClick)
    
    def on_DoubleClick(self):
        self.editor.setReadOnly(False)
        self.apply.show()
    
    def on_ApplyClick(self):
        self.editor.setReadOnly(True)
        self.apply.hide()


class songpage(qgroupbox):
    def __init__(self,texts):
        super().__init__()
        self.init(texts)
        self.setCheckable(True)
        self.setChecked(False)
        self.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop)
    
    def init(self,texts):
        self.vBox = QVBoxLayout()
        self.vBox.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop)
        self.vBox.setSizeConstraint(QLayout.SizeConstraint.SetMinimumSize)
        artist = textcell('Artist')
        artist.editor.setText(texts[0])
        album = textcell('Album')
        album.editor.setText(texts[2])
        title = textcell('Title')
        title.editor.setText(texts[1])
        self.height = 120 + artist.editor.height() + album.editor.height() + title.editor.height()
        self.vBox.addLayout(artist)
        self.vBox.addLayout(album)
        self.vBox.addLayout(title)
        print(self.children())
        print(self.vBox.children())
        print(self.vBox.count())
        print(artist.count())
        print(artist.children())
        print(artist.contentsMargins().top())
        print(artist.contentsMargins().bottom())
        print(self.vBox.contentsMargins().top())
        print(self.vBox.contentsMargins().bottom())
        print(self.contentsMargins().top())
        print(self.contentsMargins().bottom())
        print(self.childrenRect().height())
        print(self.contentsRect().height())
        print(artist.apply.isHidden())
        print(artist.editor.isHidden())
        print(artist.label.isHidden())
        print(artist.label.isVisible())
        print(self.sizeHint().height())
        self.setLayout(self.vBox)
        self.setFixedHeight(self.height)

class Window(QMainWindow):
    def __init__(self):
        super().__init__()
        self.resize(405,720)
        frame = self.frameGeometry()
        center = self.screen().availableGeometry().center()
        frame.moveCenter(center)
        self.move(frame.topLeft())
        self.centralwidget = QWidget(self)
        self.vBox = QVBoxLayout(self.centralwidget)
        self.scrollArea = QScrollArea(self.centralwidget)
        self.scrollArea.setWidgetResizable(True)
        self.scrollAreaWidgetContents = QWidget()
        self.scrollArea.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBaralwaysOn)
        self.verticalLayout = QVBoxLayout(self.scrollAreaWidgetContents)
        self.verticalLayout.setAlignment(Qt.AlignmentFlag.AlignTop)
        self.scrollArea.setWidget(self.scrollAreaWidgetContents)
        self.scrollArea.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop)
        self.add = makebutton('Add')
        self.vBox.addWidget(self.add)
        self.add.clicked.connect(lambda: adder.addItem())
        self.vBox.addWidget(self.scrollArea)
        self.setCentralWidget(self.centralwidget)

class Adder:
    def __init__(self):
        self.i = 0
    def addItem(self):
        window.verticalLayout.addWidget(songpage(items[self.i]))
        if self.i < len(items) - 1:
            self.i += 1

adder = Adder()

items = [('Herbert von Karajan',"Orphée aux enfers,'Orpheus in the Underworld'\u2014Overture",'100 Best Karajan'),('Herbert von Karajan','Radetzky march Op. 228','Symphony No. 1 in C,Op. 21\u2014I. Adagio molto \u2014 Allegro con brio','Beethoven\u2014 The 9 Symphonies'),Op. 21\u2014II. Andante cantabile con moto',Op. 21\u2014III. Menuetto (Allegro molto e vivace)',Op. 21\u2014IV. Finale (Adagio \u2014 Allegro molto e vivace)','Symphony No. 2 in D,Op. 36\u2014I. Adagio molto \u2014 Allegro con brio',Op. 36\u2014II. Larghetto',Op. 36\u2014III. Scherzo (Allegro)',Op. 36\u2014IV. Allegro molto','Symphony No. 3 in E\u2014Flat,Op. 55 \u2014Eroica\u2014I. Allegro con brio',Op. 55 \u2014Eroica\u2014II. marcia funebre (Adagio assai)',Op. 55 \u2014Eroica\u2014III. Scherzo (Allegro vivace)',Op. 55 \u2014Eroica\u2014IV. Finale (Allegro molto)','Symphony No. 4 in B\u2014Flat,Op. 60\u2014I. Adagio \u2014 Allegro vivace',Op. 60\u2014II. Adagio',Op. 60\u2014III. Allegro vivace',Op. 60\u2014IV. Allegro ma non troppo','Symphony No. 5 in C Minor,Op. 67\u2014I. Allegro con brio',Op. 67\u2014II. Andante con moto',Op. 67\u2014III. Allegro',Op. 67\u2014IV. Allegro','Symphony No. 7 in A,Op. 92\u2014I. Poco sostenuto \u2014 Vivace',Op. 92\u2014II. Allegretto',Op. 92\u2014III. Presto \u2014 Assai meno presto',Op. 92\u2014IV. Allegro con brio','Symphony No. 8 in F,Op. 93\u2014I. Allegro vivace e con brio',Op. 93\u2014II. Allegretto scherzando',Op. 93\u2014III. Tempo di menuetto',Op. 93\u2014IV. Allegro vivace','Symphony No. 9 in D Minor,Op. 125 \u2014 Choral\u2014I. Allegro ma non troppo,un poco maestoso',Op. 125 \u2014 Choral\u2014II. Molto vivace',Op. 125 \u2014 Choral\u2014III. Adagio molto e cantabile',Op. 125 \u2014 Choral\u2014IV. Presto',Op. 125 \u2014 Choral\u2014V. Presto\u2014 O Freunde,nicht diese T\u2014ne!\u2014Allegro assai','Symphony No.6 in F,Op.68 \u2014Pastoral\u2014I. Erwachen heiterer Empfindungen bei der Ankunft auf dem Lande\u2014 Allegro ma non troppo',Op.68 \u2014Pastoral\u2014II. Szene am Bach\u2014 (Andante molto mosso)',Op.68 \u2014Pastoral\u2014III. Lustiges Zusammensein der Landleute (Allegro)',Op.68 \u2014Pastoral\u2014IV. Gewitter,Sturm (Allegro)',Op.68 \u2014Pastoral\u2014V. Hirtengesang. Frohe und dankbare Gefühle nach dem Sturm\u2014 Allegretto','Cancan (Orpheus in the Underworld)','Best of the Millennium\u2014 Top 40 Classical Hits'),'Hungarian Dance No. 5 in G Minor,WoO 1 No. 5','Complete Recordings on Deutsche Grammophon')]



app = QApplication([])
window = Window()
window.show()
app.exec()

你可以看到 qgroupboxs 不是在最佳高度,如果 QTextEdits 被双击并出现应用按钮,groupBoxs 的顶部会增长,复选框处于尴尬的位置,当 QTextBoxs 调整为较小时线,底部边框将不可见。

并且布局的边距始终为零,qgroupbox 的打印边距与观察到的边距不匹配,qgroupboxs 的 childrenRects 高度始终为零,qgroupboxs 的 contentsRects 高度始终为 478,这没有任何意义,.isHidden() 将始终为 True,.isVisible() 将始终为 False,如果我只将小部件添加到布局而不调用.show()

但是当我将它们添加到布局中时,它们总是自动可见的,除非我明确地将它们设为 .hide()调用 .show() 将使已经可见的小部件在屏幕中央闪烁,除非设置了 Qt.WidgetAttribute.WA_DontShowOnScreen

那么我如何才能真正动态地计算出最佳的最小高度?

而且我不能使用 .addStretch(),因为我需要 qgroupboxs 之间的最小、最佳间距,并且我需要使用索引使内容删除.addStretch() 将破坏索引排序,因为拉伸是'当项目被移除时不会被移除。

解决方法

我终于做到了,问题是当我刚刚创建对象并没有将它们添加到主窗口时,它们都有默认大小和值,并且当它们添加到窗口时值会发生变化。

所以我只需要计算它们的大小并在我将它们添加到窗口后调整它们的大小。

在我的观察中,QGroupBoxs 中的 QVBoxLayouts 总是有 (9,9,9) 的边距,而小部件实际所在的所有布局都没有边距,而 QGroupBoxs 本身的边距为 (3,20,3,3) 在我的主脚本中,所以我可以明确设置这些边距并使用这些值。

我是这样做的:

class Label(QLabel):
    def __init__(self,text):
        super().__init__()
        self.setFont(font)
        self.fontRuler = QFontMetrics(font)
        self.setText(text)
        self.Height = self.fontRuler.size(0,text).height()
        self.Width = self.fontRuler.size(0,text).width()
        self.setFixedSize(self.Width,self.Height)


class Editor(QTextEdit):
    doubleClicked = pyqtSignal(QTextEdit)
    
    def __init__(self):
        super().__init__()
        self.setReadOnly(True)
        self.setFont(font)
        self.setFixedHeight(20)
        self.textChanged.connect(self.autoResize)
    
    def mouseDoubleClickEvent(self,e: QMouseEvent) -> None:
        self.doubleClicked.emit(self)
    
    def autoResize(self):
        self.document().setTextWidth(self.viewport().width())
        margins = self.contentsMargins()
        height = int(self.document().size().height() +
                    margins.top() + margins.bottom())
        self.setFixedHeight(height)
    
    def resizeEvent(self,e: QResizeEvent) -> None:
        self.autoResize()


class TextCell(QVBoxLayout):
    resize = pyqtSignal(QVBoxLayout)
    
    def __init__(self,text):
        super().__init__()
        self.setAlignment(Qt.AlignmentFlag.AlignLeft |
                        Qt.AlignmentFlag.AlignTop)
        self.label = Label(text)
        self.apply = makebutton('Apply')
        self.apply.hide()
        self.editor = Editor()
        self.editor.doubleClicked.connect(self.on_DoubleClick)
        self.editor.doubleClicked.connect(lambda: self.resize.emit(self))
        self.hbox = QHBoxLayout()
        self.hbox.addStretch()
        self.hbox.addWidget(self.apply)
        self.addWidget(self.label)
        self.addWidget(self.editor)
        self.addLayout(self.hbox)
        self.apply.clicked.connect(self.on_ApplyClick)
        self.apply.clicked.connect(lambda: self.resize.emit(self))
    
    def on_DoubleClick(self):
        self.editor.setReadOnly(False)
        self.apply.show()
    
    def on_ApplyClick(self):
        self.editor.setReadOnly(True)
        self.apply.hide()
    
    def get_Height(self):
        height = 0
        for i in range(self.count()):
            item = self.itemAt(i)
            if item.widget():
                widget = item.widget()
            else:
                widget = item.itemAt(1).widget()
            if not widget.isHidden():
                height += widget.height()
                height += 9
        return height


class SongPage(QGroupBox):
    def __init__(self,texts):
        super().__init__()
        self.init(texts)
        self.setCheckable(True)
        self.setChecked(False)
        self.setAlignment(Qt.AlignmentFlag.AlignLeft |
                        Qt.AlignmentFlag.AlignTop)
    
    def init(self,texts):
        self.vbox = QVBoxLayout()
        self.vbox.setAlignment(Qt.AlignmentFlag.AlignLeft |
                            Qt.AlignmentFlag.AlignTop)
        self.vbox.setSizeConstraint(QLayout.SizeConstraint.SetMinimumSize)
        self.artist = TextCell('Artist')
        self.artist.editor.setText(texts[0])
        self.album = TextCell('Album')
        self.album.editor.setText(texts[1])
        self.title = TextCell('Title')
        self.title.editor.setText(texts[2])
        self.vbox.addLayout(self.artist)
        self.vbox.addLayout(self.album)
        self.vbox.addLayout(self.title)
        self.setContentsMargins(3,3)
        self.vbox.setContentsMargins(9,9)
        self.setLayout(self.vbox)
        self.artist.resize.connect(self.autoResize)
        self.album.resize.connect(self.autoResize)
        self.title.resize.connect(self.autoResize)
    
    def autoResize(self):
        height = sum(i.get_Height() for i in self.vbox.children()) + 23
        self.setFixedHeight(height)
    
    def resizeEvent(self,e: QResizeEvent) -> None:
        self.autoResize()

QGroupBoxs 会正确自动调整大小,唯一的问题是复选框的位置,在我的主脚本中,复选框位于正确的位置,但在具有相同类的另一个脚本中,复选框位于错误的位置,但这并没有影响主脚本。

当 QTextEdits 缩小时,它们的底部边框变得不可见。