Premise: this is more an explanation than a solution, but being aware of these aspects is extremely important.
tl;dr
Your perception of "line count" is mislead, as lines are counted based on the presence of actual "new line" objects, not just as they are displayed.
If you want to know the visual line count, you need to go through the QTextDocument API (specifically, through the QTextLayout interface).
Line count in computing is not about display
In computing, lines are not counted based on how they are shown (that's also one of the reason for which you cannot evaluate the quality of code based on the line count).
You're trying to get the visual lines, but standard editors count lines based on their characters, not on how those characters are rendered. The concept is similar to model and view: what you see is an abstraction of the actual data, and since that abstraction can have complex ways of displaying that data, counting the "lines" makes little sense in normal conditions.
Suppose that you have a table with lots of columns, each cell is so small that it can only show one character and that forces its displaying to wrap every single letter. How would you count "lines" in that case? You just can't, and it would be pointless.
The concept of line count is an old reminiscence of written text layout (notably become common with typewriters, which had fixed character sizes), but it's just a simplification that in modern terms has little meaning.
Still, if you really want the "visible" line count, you have to go through the QTextDocument API and get the visual line count from it.
How are lines actually counted
A common, standard text editor does not count visible lines, but actual ones: those that explicitly end with a new line as the user intended.
For basic text editors, that means that the actual line count is the number of line terminators, eventually including the last character that is not a new line. Note that on *nix all plain text files normally end with a line terminator (see this related post).
This is an important assumption when dealing with visual editors: they must not show the "visible" line count whenever they support visual wrapping.
Suppose that you have this content in a plain text file:
This is a short line.
This is a very, very, very, very, very, very, very, very, very, very, very, very, very, very, very, very, very, very, very, very, very, very, very, very, extremely and annoyingly long line.
You will probably see only two lines, and will need to scroll the above in order to see its contents. That's because there is only one "new line character", which is the one at the end of the first "short" line. Or, to be precise, there are actually two, as there is another at the end of the longer line.
Now, let's see the same content in a more "readable" way:
This is a short line.
This is a very, very, very, very, very, very, very, very, very, very, very, very, very, very, very, very, very, very, very, very, very, very, very, very, extremely and annoyingly long line.
If you see it in a standard computer screen it will be probably shown in at least three lines, and even much more if you're using a mobile device.
In reality, the content is exactly the same, the difference is only how the visual rendering decides to wrap those lines.
That's not unlike comparing concepts or phrases and how they are printed on a book or displayed on a screen.
Does
this
seem
like
an
eight
lines
phrase?
QTextDocument and its layout interface
The QTextDocument framework is primarily intended for displaying purposes, while keeping a consistent API for upper level implementation. This means that it still exposes the "line count" concept as explained above. Each text block represents a paragraph that has at least a "line" (but it can have multiple lines in it, not only visual ones based on the wrapping mode, but also actual ones).
A QTextEdit widget internally sets a textWidth
on the document whenever it's resized and based on its wrapping rules (including specific no-wrap rules for text fragments).
Since the internal QTextDocument is paired with the QTextEdit, you can get the visual line count of each block by accessing its layout()
.
You can have the total visual line count by iterating through each text block and get the line count from the block text layout:
class LineCountTextEdit(QTextEdit):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.textChanged.connect(self.updateLineNumbers)
def updateLineNumbers(self):
lineCount = 0
block = self.document().begin()
while block.isValid():
lineCount += block.layout().lineCount()
block = block.next()
if not lineCount:
lineCount = 1 # there is *always* at least one line!
print('visual line count: {}'.format(lineCount))
def resizeEvent(self, event):
super().resizeEvent(event)
self.updateLineNumbers()
Implementing a valid line count visualization
Even considering the above, your implementation for the line count visualization is wrong, as you did not consider the following aspects:
- the most important one, vertical scrolling: if the text contents become taller than the widget height and the user scrolls it, the contents of the label will be inconsistent;
- the height of each line (QTextEdit supports rich text, and a line could have fonts with a different heights);
- the margins of the QTextEdit widget frame, and those of the document;
A proper line visualization should be embedded within the widget itself (either as a directly related subclass, or with custom internal behavior), and it must implement custom painting.
The following example shows a possible implementation that considers all aspects explained above.
class LineCountTextEdit(QTextEdit):
lineCount = 0
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.textChanged.connect(self.updateLineNumbers)
self.verticalScrollBar().rangeChanged.connect(self.update)
self.verticalScrollBar().valueChanged.connect(self.update)
def event(self, event):
if event.type() == event.Paint:
# we cannot override paintEvent(), because it has effect on the
# viewport and we need to directly paint on the widget.
super().event(event)
self.paintLineNumbers()
return True
return super().event(event)
def paintLineNumbers(self):
qp = QPainter(self)
fm = self.fontMetrics()
margins = self.contentsMargins()
left = margins.left()
viewMargin = self.viewportMargins().left()
rightMargin = viewMargin - (fm.horizontalAdvance(' ') + left)
viewTop = margins.top()
viewHeight = self.height() - (viewTop + margins.bottom())
qp.setClipRect(
viewTop, margins.left(),
viewMargin, viewHeight
)
qp.translate(margins.left(), 0)
top = self.verticalScrollBar().value()
bottom = top + viewHeight
offset = viewTop - self.verticalScrollBar().value()
lineCount = 1
doc = self.document()
docLayout = doc.documentLayout()
block = doc.begin()
while block.isValid():
blockRect = docLayout.blockBoundingRect(block)
blockTop = blockRect.y()
blockLayout = block.layout()
blockLineCount = blockLayout.lineCount()
if (
blockRect.bottom() >= top
and blockTop + offset <= bottom
):
# only draw a text block if its bounding rect is visible
for l in range(blockLineCount):
line = blockLayout.lineAt(l)
qp.drawText(
left, offset + blockTop + line.y(),
rightMargin, line.height(),
Qt.AlignRight, str(lineCount)
)
lineCount += 1
else:
lineCount += blockLineCount
block = block.next()
def updateLineNumbers(self):
lineCount = 0
block = self.document().begin()
while block.isValid():
lineCount += block.layout().lineCount()
block = block.next()
if lineCount < 1:
lineCount = 1
if self.lineCount != lineCount:
self.lineCount = lineCount
countLength = len(str(self.lineCount))
margin = self.fontMetrics().horizontalAdvance(' ' + '0' * countLength)
if self.viewportMargins().left() != margin:
self.setViewportMargins(margin, 0, 0, 0)
self.update()
def resizeEvent(self, event):
super().resizeEvent(event)
self.updateLineNumbers()
class TextEditor(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("e-dit")
self.label = QLabel()
self.label.setText('Ready')
self.statusBar().addPermanentWidget(self.label)
self.txt = LineCountTextEdit(self)
self.txt.setAlignment(Qt.AlignRight)
self.setCentralWidget(self.txt)
if __name__ == '__main__':
app = QApplication(sys.argv)
editor = TextEditor()
editor.show()
sys.exit(app.exec())
Note that I ignored the QSS aspect of your question: you shall only ask one question per post, unless they are strictly related. Besides, you should never set generic properties on a parent widget unless you know what you're doing and how style sheet properties propagate on children. Rmove the top level setStyleSheet
and see the difference in the scroll bar as soon as it's shown.