from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow")
MainWindow.resize(800, 130)
self.centralwidget = QtWidgets.QWidget(MainWindow)
self.centralwidget.setObjectName("centralwidget")
self.gridLayout = QtWidgets.QGridLayout(self.centralwidget)
self.gridLayout.setObjectName("gridLayout")
self.plainTextEdit = QtWidgets.QPlainTextEdit(self.centralwidget)
self.plainTextEdit.setStyleSheet("padding:0px;")
self.plainTextEdit.setObjectName("plainTextEdit")
self.gridLayout.addWidget(self.plainTextEdit, 0, 0, 1, 1)
MainWindow.setCentralWidget(self.centralwidget)
self.menubar = QtWidgets.QMenuBar(MainWindow)
self.menubar.setGeometry(QtCore.QRect(0, 0, 800, 21))
self.menubar.setObjectName("menubar")
MainWindow.setMenuBar(self.menubar)
self.statusbar = QtWidgets.QStatusBar(MainWindow)
self.statusbar.setObjectName("statusbar")
MainWindow.setStatusBar(self.statusbar)
self.retranslateUi(MainWindow)
QtCore.QMetaObject.connectSlotsByName(MainWindow)
def retranslateUi(self, MainWindow):
_translate = QtCore.QCoreApplication.translate
MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
self.plainTextEdit.setPlainText(_translate("MainWindow", "1\n"
"1\n"
"1\n"
"1\n"
"1\n"
"1\n"
"1\n"
"1\n"
"1\n"
"1\n"
"1\n"
"1\n"
"last line."))
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
MainWindow = QtWidgets.QMainWindow()
ui = Ui_MainWindow()
ui.setupUi(MainWindow)
MainWindow.show()
sys.exit(app.exec_())
Result:
Some work-arounds:
#self.main_self.ui_scheduled_transmitions_create_window.review_text.setPlainText("")
self.main_self.ui_scheduled_transmitions_create_window.review_text.setPlainText(review_text)
self.main_self.ui_scheduled_transmitions_create_window.review_text.setDocumentTitle("123")
#if self.main_self.ui_scheduled_transmitions_create_window.review_text.height()<800:
# self.main_self.ui_scheduled_transmitions_create_window.review_text.setFixedHeight(self.main_self.ui_scheduled_transmitions_create_window.review_text.height())
#else:
# self.main_self.ui_scheduled_transmitions_create_window.review_text.setFixedHeight(800)
self.main_self.ui_scheduled_transmitions_create_window.review_text.document().setDocumentMargin(0)
#self.main_self.ui_scheduled_transmitions_create_window.review_text.setCenterOnScroll(False)
m = self.main_self.ui_scheduled_transmitions_create_window.review_text.fontMetrics()
RowHeight = m.lineSpacing()
nRows = self.main_self.ui_scheduled_transmitions_create_window.review_text.document().blockCount()
text_height = RowHeight*nRows
document_format = self.main_self.ui_scheduled_transmitions_create_window.review_text.document().rootFrame().frameFormat()
document_format.setBottomMargin(0)
document_format.setHeight(text_height)
self.main_self.ui_scheduled_transmitions_create_window.review_text.document().rootFrame().setFrameFormat(document_format)
scrollBarHeight=self.main_self.ui_scheduled_transmitions_create_window.review_text.horizontalScrollBar().sizeHint().height()
#scrollBarHeight=0
if text_height+scrollBarHeight>800:
self.main_self.ui_scheduled_transmitions_create_window.review_text.setFixedHeight(600)
else:
self.main_self.ui_scheduled_transmitions_create_window.review_text.setFixedHeight(text_height+scrollBarHeight)

QPlainTextEdit pros and cons
The problem with QPlainTextEdit is that, due to its optimizations, it has some peculiar aspects when dealing with vertical scrolling and text positioning.
In fact, its default QTextDocument uses a special QAbstractTextDocumentLayout subclass (QPlainTextDocumentLayout), which:
The vertical scroll bar itself has a different behavior, and when it's triggered it actually scrolls the contents based on the position of its block and their lines, instead of working in pixel deltas (as opposed to what a normal scroll area would, including QTextEdit), and eventually adjusts the scroll bar value accordingly.
It practically works in a way similar to the default
scrollModeof item views:ScrollPerItem(aka, "scroll per line") instead ofScrollPerPixel.Its maximum value, in fact, is the total line number minus the visible line count (including line wrapping for both): if you have 10 lines and you can see 6 full lines, the scroll bar range is only of 4.
The bottom margin that you see is caused by the fact that QPlainTextEdit always shows the first visible line completely, even if that results in displaying that bottom margin way beyond the height of the last line: for this class, the priority is to show any top line in its full height.
Unfortunately, working around this may be a bit complex due to the altered behavior above. Also, that same behavior results in some cases for which the
valueChangedsignal of the scroll bar is never emitted at all, because the value adjustment explained above is done with a QSignalBlocker context that prevents any connected function to receive its changes.A QTextEdit substitute
As long as the contents of the editor are not that big (which is the common reason for using QPlainTextEdit), there is a simpler solution: use a QTextEdit instead and fix the scroll bar position while it's being updated.
This is possible exactly because QTextEdit uses pixel precision (instead of lines), meaning that changing the scroll bar value by 1 results in a single pixel scrolling of the viewport.
In order to achieve the above, we have to remember that widgets that inherit from QAbstractSlider (like QSlider and QScrollBar) have two different properties that indicate their values:
sliderPosition: the "visible" slider position;value: the "final" value exposed by the slider, which is what scroll areas actually use to scroll the viewport;Even if, for normal usage, both values normally coincide (unless
trackingis disabled), internally they are not updated at the same time: the slider is "moved" first, but the value is updated after.The
actionTriggeredsignal is triggered exactly when "something" tells the slider to move (by using the mouse, the wheel or the arrow buttons), but the value has not changed yet; as the documentation explains:The final line is what interests us more: we can fix up the slider position before its value has been propagated, so that when it will be actually applied, it will use the final value we set in
setSliderPosition().Considering that the scroll bar moves the viewport in unit/pixels, we assume that the value would need to be an exact multiple of the line height.
We just take the current slider position, use a floor division (
//) with the font metrics'height()to get the line count, and multiply it back by the height again. The divide/multiply is necessary to get a precise multiple of the line height so that it "snaps" at the correct position.For instance, assuming we have a line height of 15 pixels and the slider position at 40, the resulting formula would be the following:
Alternatively, for extremely accurate scrolling precision movement, you could check whether the modulo division returns a rest that is higher than half the font height and eventually add a further line;
divmod(40, 15)would return2, 10, solineCountwould actually be 3, meaning that the scrolling to the next line would happen a few "scroll bar pixels" before.Finally, we need to consider some further factors:
SliderNoAction);sliderPosition()is equal to the maximum, we should just scroll to the bottom (so, without doing any of the above);acceptRichTextmust be set toFalse;Actual example
Here is a possible implementation of the subclass:
Note that, for obvious reasons, you can still call
setValue()on the scroll bar using arbitrary values that do not match lines precisely.