Here's an MRE:
import sys, random
from PyQt5 import QtWidgets, QtCore, QtGui
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
main_splitter = MainSplitter(self)
self.setCentralWidget(main_splitter)
main_splitter.setOrientation(QtCore.Qt.Vertical)
main_splitter.setStyleSheet('background-color: red; border: 1px solid pink;');
# top component of vertical splitter: a QFrame to hold horizontal splitter and breadcrumbs QPlainTextEdit
top_frame = QtWidgets.QFrame()
top_frame_layout = QtWidgets.QVBoxLayout()
top_frame.setLayout(top_frame_layout)
top_frame.setStyleSheet('background-color: green; border: 1px solid green;');
main_splitter.addWidget(top_frame)
# top component of top_frame: horizontal splitter
h_splitter = HorizontalSplitter()
h_splitter.setStyleSheet('background-color: cyan; border: 1px solid orange;')
top_frame_layout.addWidget(h_splitter)
# bottom component of top_frame: QPlainTextEdit (for "breadcrumbs")
self.breadcrumbs_pte = BreadcrumbsPTE(top_frame)
top_frame_layout.addWidget(self.breadcrumbs_pte)
self.text = 'some plain text '
self.breadcrumbs_pte.setPlainText(self.text * 50)
self.breadcrumbs_pte.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents)
self.breadcrumbs_pte.setStyleSheet('background-color: orange; border: 1px solid blue;');
self.breadcrumbs_pte.setMinimumWidth(300)
self.breadcrumbs_pte.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
# bottom component of vertical splitter: a QFrame
bottom_panel = BottomPanel(main_splitter)
bottom_panel.setStyleSheet('background-color: magenta; border: 1px solid cyan;')
main_splitter.addWidget(bottom_panel)
def resizeEvent(self, *args):
print('resize event...')
n_repeat = random.randint(10, 50)
self.breadcrumbs_pte.setPlainText(self.text * n_repeat)
super().resizeEvent(*args)
class BreadcrumbsPTE(QtWidgets.QPlainTextEdit):
def sizeHint(self):
return QtCore.QSize(500, 100)
class MainSplitter(QtWidgets.QSplitter):
def sizeHint(self):
return QtCore.QSize(40, 150)
class HorizontalSplitter(QtWidgets.QSplitter):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setOrientation(QtCore.Qt.Horizontal)
self.left_panel = LeftPanel()
self.left_panel.setStyleSheet('background-color: yellow; border: 1px solid black;');
self.addWidget(self.left_panel)
self.left_panel.setMinimumHeight(150)
right_panel = QtWidgets.QFrame()
right_panel.setStyleSheet('background-color: black; border: 1px solid blue;');
self.addWidget(right_panel)
# to achieve 66%-33% widths ratio
self.setStretchFactor(0, 20)
self.setStretchFactor(1, 10)
def sizeHint(self):
return self.left_panel.sizeHint()
class LeftPanel(QtWidgets.QFrame):
def sizeHint(self):
return QtCore.QSize(150, 250)
class BottomPanel(QtWidgets.QFrame):
def sizeHint(self):
return QtCore.QSize(30, 180)
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()
If you run it, you'll see that if you adjust the size of the window the text in the QPlainTextEdit
changes each time.
What I'm trying to achieve: I want the text in the QPlainTextEdit
to adjust the size of that component and the component above it (HorizontalSplitter
) so that the QPlainTextEdit
just contains perfectly the text, not leaving space at the bottom.
I want that to happen so that no adjustment to the size of the main window occurs (obviously if this happened, this would, as the code is written, currently lead to infinite triggering of the MainWindow.resizeEvent()
).
The tutorials on Qt/PyQt just don't seem to give a comprehensive technical explanation of how all the various mechanisms work and how they interact. For example, I know that sizeHint
plays a crucial role relating to sizing and layouts, but, short of trying to examine the source code in depth, I don't know how I can improve my understanding.
For example, I tried infinite permutations of commenting out sizeHint
on the various classes here, including commenting them all out: but self.top_pte.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents)
never seems to work (i.e. as I want it to!).
NB if necessary, BottomPanel
(i.e. 2nd child of the vertical QSplitter
) can be set with minimum and maximum height (i.e. fixed height) to make things simpler. The main aim is to get the horizontal QSplitter
and the QPlainTextEdit
to adjust so that the latter's height is just perfectly adjusted for the text it contains...
I found an answer after several hours of experimenting. I don't know whether some of these techniques might be considered controversial (or just bad). If musicamante takes a look at this I'd be interested in your view.
import sys, random
from PyQt5 import QtWidgets, QtCore, QtGui
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
main_splitter = MainSplitter(self)
self.setCentralWidget(main_splitter)
main_splitter.setOrientation(QtCore.Qt.Vertical)
main_splitter.setStyleSheet('background-color: red; border: 1px solid pink;');
# top component of vertical splitter: a QFrame to hold horizontal splitter and breadcrumbs QPlainTextEdit
top_frame = TopFrame(self)
top_frame_layout = QtWidgets.QVBoxLayout()
top_frame.setLayout(top_frame_layout)
top_frame.setStyleSheet('background-color: green; border: 1px solid green;');
main_splitter.addWidget(top_frame)
# top component of top_frame: horizontal splitter
h_splitter = HorizontalSplitter()
h_splitter.setStyleSheet('background-color: cyan; border: 1px solid orange;')
top_frame_layout.addWidget(h_splitter, stretch=1)
# bottom component of top_frame: QPlainTextEdit (for "breadcrumbs")
self.breadcrumbs_pte = BreadcrumbsPTE(top_frame)
top_frame_layout.addWidget(self.breadcrumbs_pte)
self.text = 'some plain text '
self.breadcrumbs_pte.setPlainText(self.text * 50 + '... END')
self.breadcrumbs_pte.setStyleSheet('background-color: orange; border: 1px solid blue;');
self.breadcrumbs_pte.setMinimumWidth(300)
# bottom component of vertical splitter: a QFrame
bottom_panel = BottomPanel(main_splitter)
bottom_panel.setStyleSheet('background-color: magenta; border: 1px solid cyan;')
main_splitter.addWidget(bottom_panel)
def resizeEvent(self, *args):
print('WINDOW resize event...')
n_repeat = random.randint(10, 50)
self.breadcrumbs_pte.setPlainText(self.text * n_repeat + '... END')
super().resizeEvent(*args)
self.breadcrumbs_pte.resizeEvent(*args)
class TopFrame(QtWidgets.QFrame):
pass
class BreadcrumbsPTE(QtWidgets.QPlainTextEdit):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
# print(f'+++ self.wordWrapMode() {self.wordWrapMode()}') # 4 (QTextOption::WrapAtWordBoundaryOrAnywhere)
def resizeEvent(self, *args):
print('BPTE resize event...')
plain_text = self.document().toPlainText()
print(f'+++ RESIZE len(plain_text) {len(plain_text)}')
super().resizeEvent(*args)
# fortunately it seemed possible to avoid this sort of technique:
# QtCore.QTimer.singleShot(0, self.updateGeometry)
self.updateGeometry()
def sizeHint(self):
super_hint = super().sizeHint()
actual_size = self.size()
print(f'+++ super_hint {super_hint} actual_size {actual_size}') #
document = QtGui.QTextDocument()
plain_text = self.document().toPlainText()
# print(f'+++ len(plain_text) {len(plain_text)}')
document.setPlainText(plain_text)
document.setTextWidth(float(actual_size.width()))
document_float_size = document.size()
document_int_size = QtCore.QSize(int(document_float_size.width()), int(document_float_size.height()))
# print(f'+++ document_int_size {document_int_size}')
self.updateGeometry()
return document_int_size
def minimumSizeHint(self):
return self.sizeHint()
class MainSplitter(QtWidgets.QSplitter):
pass
class HorizontalSplitter(QtWidgets.QSplitter):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setOrientation(QtCore.Qt.Horizontal)
self.left_panel = LeftPanel()
self.left_panel.setStyleSheet('background-color: yellow; border: 1px solid black;');
self.addWidget(self.left_panel)
self.left_panel.setMinimumHeight(150)
right_panel = QtWidgets.QFrame()
right_panel.setStyleSheet('background-color: black; border: 1px solid blue;');
self.addWidget(right_panel)
# to achieve 66%-33% widths ratio
self.setStretchFactor(0, 20)
self.setStretchFactor(1, 10)
def sizeHint(self):
return self.left_panel.sizeHint()
class LeftPanel(QtWidgets.QFrame):
def sizeHint(self):
return QtCore.QSize(150, 500)
class BottomPanel(QtWidgets.QFrame):
def sizeHint(self):
return QtCore.QSize(30, 180)
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()
I noticed that you could quite often get a main window resize event with nothing else (e.g. sizeHint
on other components) happening ... so thought the best thing to do was to relay this resizeEvent
directly on to the BreadcrumbsPTE
. I don't know whether there's a better way of forcing the descendant widgets to trigger resizeEvent
. Later yes, PTE has a signal textChanged
...
sizeHint
triggers updateGeometry
, and minimumSizeHint
obviously just runs sizeHint
, so it does that too. But BreadcrumbsPTE.resizeEvent
also seems to need to trigger updateGeometry
for things to work.
I was puzzled by the QTextDocument
delivered by BreadcrumbsPTE.document
in BreadcrumbsPTE.sizeHint
: I couldn't seem to get this to change size: maybe it's constrained by its parent (or container), i.e. the BreadcrumbsPTE
: for that reason I seemed to have to make a separate QTextDocument
fill it with the text and set its width ... in order for it to tell me what the height of the wrapped text would then be...