I am using Python 3.9.5 and PyQt6.
Following my previous questions, I want to ask about how to dynamically how to resize QGroupBox to fit its contents.
I have a QScrollArea, its layout is a QVBoxLayout, and a bunch of QGroupBoxs will be added to the QVBoxLayout.
The QGroupBoxs themselves have a QVBoxLayout, and inside the QVBoxLayout are a bunch of QVBoxLayouts, and inside the QVBoxLayouts inside the QVBoxLayout of the QGroupBox are the contents.
Each lowest level QVBoxLayout has two widgets and a QHBoxLayout.
From top to bottom, the first widget is a QLabel, which is fixed-sized 100x20, and the second widget is a QTextEdit which auto-resizes to content. And inside the QHBoxLayout is a stretch and a fixed-sized button with size of 60x20, the button can either be hidden or shown.
The hierarchy of the widgets and layouts is this:
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)
I want the QGroupBoxs to automatically resize to fit their contents.
The width is determined by layout, and I want the QGroupBoxs have a fixed, minimal, optimal height based on its contents.
Basically, the height should be the sum of the heights of all its VISIBLE widgets, plus the top margin and bottom margin, and margin between the widgets, and perhaps the height of the checkboxs.
I want to set fixed heights for the QGroupBoxs because if I don't do so, the layout will stretch the QGroupBoxs and position it in the middle, rather than at the top with minimal height which isn't what I wanted.
I will post example codes below:
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', '100 Best Karajan'),
('Herbert von Karajan',
'Symphony No. 1 in C, Op. 21\u2014I. Adagio molto \u2014 Allegro con brio',
'Beethoven\u2014 The 9 Symphonies'),
('Herbert von Karajan',
'Symphony No. 1 in C, Op. 21\u2014II. Andante cantabile con moto',
'Beethoven\u2014 The 9 Symphonies'),
('Herbert von Karajan',
'Symphony No. 1 in C, Op. 21\u2014III. Menuetto (Allegro molto e vivace)',
'Beethoven\u2014 The 9 Symphonies'),
('Herbert von Karajan',
'Symphony No. 1 in C, Op. 21\u2014IV. Finale (Adagio \u2014 Allegro molto e vivace)',
'Beethoven\u2014 The 9 Symphonies'),
('Herbert von Karajan',
'Symphony No. 2 in D, Op. 36\u2014I. Adagio molto \u2014 Allegro con brio',
'Beethoven\u2014 The 9 Symphonies'),
('Herbert von Karajan',
'Symphony No. 2 in D, Op. 36\u2014II. Larghetto',
'Beethoven\u2014 The 9 Symphonies'),
('Herbert von Karajan',
'Symphony No. 2 in D, Op. 36\u2014III. Scherzo (Allegro)',
'Beethoven\u2014 The 9 Symphonies'),
('Herbert von Karajan',
'Symphony No. 2 in D, Op. 36\u2014IV. Allegro molto',
'Beethoven\u2014 The 9 Symphonies'),
('Herbert von Karajan',
'Symphony No. 3 in E\u2014Flat, Op. 55 \u2014Eroica\u2014I. Allegro con brio',
'Beethoven\u2014 The 9 Symphonies'),
('Herbert von Karajan',
'Symphony No. 3 in E\u2014Flat, Op. 55 \u2014Eroica\u2014II. Marcia funebre (Adagio assai)',
'Beethoven\u2014 The 9 Symphonies'),
('Herbert von Karajan',
'Symphony No. 3 in E\u2014Flat, Op. 55 \u2014Eroica\u2014III. Scherzo (Allegro vivace)',
'Beethoven\u2014 The 9 Symphonies'),
('Herbert von Karajan',
'Symphony No. 3 in E\u2014Flat, Op. 55 \u2014Eroica\u2014IV. Finale (Allegro molto)',
'Beethoven\u2014 The 9 Symphonies'),
('Herbert von Karajan',
'Symphony No. 4 in B\u2014Flat, Op. 60\u2014I. Adagio \u2014 Allegro vivace',
'Beethoven\u2014 The 9 Symphonies'),
('Herbert von Karajan',
'Symphony No. 4 in B\u2014Flat, Op. 60\u2014II. Adagio',
'Beethoven\u2014 The 9 Symphonies'),
('Herbert von Karajan',
'Symphony No. 4 in B\u2014Flat, Op. 60\u2014III. Allegro vivace',
'Beethoven\u2014 The 9 Symphonies'),
('Herbert von Karajan',
'Symphony No. 4 in B\u2014Flat, Op. 60\u2014IV. Allegro ma non troppo',
'Beethoven\u2014 The 9 Symphonies'),
('Herbert von Karajan',
'Symphony No. 5 in C Minor, Op. 67\u2014I. Allegro con brio',
'Beethoven\u2014 The 9 Symphonies'),
('Herbert von Karajan',
'Symphony No. 5 in C Minor, Op. 67\u2014II. Andante con moto',
'Beethoven\u2014 The 9 Symphonies'),
('Herbert von Karajan',
'Symphony No. 5 in C Minor, Op. 67\u2014III. Allegro',
'Beethoven\u2014 The 9 Symphonies'),
('Herbert von Karajan',
'Symphony No. 5 in C Minor, Op. 67\u2014IV. Allegro',
'Beethoven\u2014 The 9 Symphonies'),
('Herbert von Karajan',
'Symphony No. 7 in A, Op. 92\u2014I. Poco sostenuto \u2014 Vivace',
'Beethoven\u2014 The 9 Symphonies'),
('Herbert von Karajan',
'Symphony No. 7 in A, Op. 92\u2014II. Allegretto',
'Beethoven\u2014 The 9 Symphonies'),
('Herbert von Karajan',
'Symphony No. 7 in A, Op. 92\u2014III. Presto \u2014 Assai meno presto',
'Beethoven\u2014 The 9 Symphonies'),
('Herbert von Karajan',
'Symphony No. 7 in A, Op. 92\u2014IV. Allegro con brio',
'Beethoven\u2014 The 9 Symphonies'),
('Herbert von Karajan',
'Symphony No. 8 in F, Op. 93\u2014I. Allegro vivace e con brio',
'Beethoven\u2014 The 9 Symphonies'),
('Herbert von Karajan',
'Symphony No. 8 in F, Op. 93\u2014II. Allegretto scherzando',
'Beethoven\u2014 The 9 Symphonies'),
('Herbert von Karajan',
'Symphony No. 8 in F, Op. 93\u2014III. Tempo di menuetto',
'Beethoven\u2014 The 9 Symphonies'),
('Herbert von Karajan',
'Symphony No. 8 in F, Op. 93\u2014IV. Allegro vivace',
'Beethoven\u2014 The 9 Symphonies'),
('Herbert von Karajan',
'Symphony No. 9 in D Minor, Op. 125 \u2014 Choral\u2014I. Allegro ma non troppo, un poco maestoso',
'Beethoven\u2014 The 9 Symphonies'),
('Herbert von Karajan',
'Symphony No. 9 in D Minor, Op. 125 \u2014 Choral\u2014II. Molto vivace',
'Beethoven\u2014 The 9 Symphonies'),
('Herbert von Karajan',
'Symphony No. 9 in D Minor, Op. 125 \u2014 Choral\u2014III. Adagio molto e cantabile',
'Beethoven\u2014 The 9 Symphonies'),
('Herbert von Karajan',
'Symphony No. 9 in D Minor, Op. 125 \u2014 Choral\u2014IV. Presto',
'Beethoven\u2014 The 9 Symphonies'),
('Herbert von Karajan',
'Symphony No. 9 in D Minor, Op. 125 \u2014 Choral\u2014V. Presto\u2014 O Freunde, nicht diese T\u2014ne!\u2014Allegro assai',
'Beethoven\u2014 The 9 Symphonies'),
('Herbert von Karajan',
'Symphony No.6 in F, Op.68 \u2014Pastoral\u2014I. Erwachen heiterer Empfindungen bei der Ankunft auf dem Lande\u2014 Allegro ma non troppo',
'Beethoven\u2014 The 9 Symphonies'),
('Herbert von Karajan',
'Symphony No.6 in F, Op.68 \u2014Pastoral\u2014II. Szene am Bach\u2014 (Andante molto mosso)',
'Beethoven\u2014 The 9 Symphonies'),
('Herbert von Karajan',
'Symphony No.6 in F, Op.68 \u2014Pastoral\u2014III. Lustiges Zusammensein der Landleute (Allegro)',
'Beethoven\u2014 The 9 Symphonies'),
('Herbert von Karajan',
'Symphony No.6 in F, Op.68 \u2014Pastoral\u2014IV. Gewitter, Sturm (Allegro)',
'Beethoven\u2014 The 9 Symphonies'),
('Herbert von Karajan',
'Symphony No.6 in F, Op.68 \u2014Pastoral\u2014V. Hirtengesang. Frohe und dankbare Gefühle nach dem Sturm\u2014 Allegretto',
'Beethoven\u2014 The 9 Symphonies'),
('Herbert von Karajan',
'Cancan (Orpheus in the Underworld)',
'Best of the Millennium\u2014 Top 40 Classical Hits'),
('Herbert von Karajan',
'Hungarian Dance No. 5 in G Minor, WoO 1 No. 5',
'Complete Recordings on Deutsche Grammophon')]
app = QApplication([])
window = Window()
window.show()
app.exec()
You can see the QGroupBoxs aren't at optimal height, and if the QTextEdits are double clicked and the Apply buttons appeared, the top part of the groupboxs will grow, the checkboxs are at awkward positions, when the QTextBoxs resized to less lines, the bottom border will become invisible.
And the margins of the layouts are always zero, the printed margins of the QGroupBox don't match the margins observed, the childrenRects' height of the QGroupBoxs are always zero, and the contentsRects' height of the QGroupBoxs are always 478, which doesn't make any sense, and .isHidden()
will always be True, .isVisible()
will always be False, if I only added the widget to the layout without calling its .show()
.
But when I add them to the layouts, they are always automatically visible, unless I explicitly make them .hide()
. And calling .show()
will make already visible widget flash at the center of the screen, unless Qt.WidgetAttribute.WA_DontShowOnScreen
is set.
So how can I actually calculate the optimal, minimal height dynamically?
And I can't use .addStretch()
because I need minimal, optimal spacing between the QGroupBoxs and I need to make the contents deletable by using indexes, .addStretch()
will break the indexes ordering since the stretchs aren't removed when the items are removed.
I have finally did it, the problem is when I just created the objects and didn't add them to the main window, they all have default sizes and values, and the value changes when they are added to the window.
So I just need to calculate their sizes and resize them AFTER I have added them to the window.
In my observation, the QVBoxLayouts in the QGroupBoxs always have margins of (9, 9, 9, 9), and all the layouts where the widgets actually are don't have margins, and the QGroupBoxs themselves have margins of (3, 20, 3, 3) in my main script, so I could just explicitly set these margins and use these values.
This is how I have done it:
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, 20, 3, 3)
self.vbox.setContentsMargins(9, 9, 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()
The QGroupBoxs auto-resize properly, the only problems are the positions of the checkboxs, in my main script the checkboxs are in correct positions but in another script with the same classes the checkboxs are in wrong positions, but that doesn't affect the main script.
And when the QTextEdits are shrunk, their bottom borders become invisible.