3

I tried to learn some Qt (PyQt). To do so I used the Code Editor example of the docs. The highlighting of the current line works fine. The line number however do not show.

Actually the LineNumberArea.paintEvent is not even called. Subequently also not CodeEditor.lineNumberAreaPaintEvent. As far as I understood it the paintEvent of the number bars should be called by periodically. Or at least when there is an updateRequest or scroll event (called by CodeEditor.updateLineNumberArea).

Here is my take to port the code from c++ to python:

import sys
from PyQt4.QtGui import *
from PyQt4.QtCore import *

import numpy as np

class LineNumberArea(QWidget):
    def __init__(self, editor):
        super().__init__()
        self.editor = editor


    def sizeHint(self):
        return Qsize(self.editor.lineNumberAreaWidth(), 0)


    def paintEvent(self, event):
        print('LineNumberArea.paintEvent')
        self.editor.lineNumberAreaPaintEvent(event)


class CodeEditor(QPlainTextEdit):
    def __init__(self):
        super().__init__()
        self.lineNumberArea = LineNumberArea(self)

        self.connect(self, SIGNAL('blockCountChanged(int)'), self.updateLineNumberAreaWidth)
        self.connect(self, SIGNAL('updateRequest(QRect,int)'), self.updateLineNumberArea)
        self.connect(self, SIGNAL('cursorPositionChanged()'), self.highlightCurrentLine)

        self.updateLineNumberAreaWidth(0)


    def lineNumberAreaWidth(self):
        """ This method has been slightly modified (use of log and uses actual
        font rather than standart.) """
        n_lines = self.blockCount()
        digits = np.ceil(np.log10(n_lines)) + 1
        return digits * QFontMetrics(self.font()).width('9') + 3


    def updateLineNumberAreaWidth(self, _):
        print('CodeEditor.updateLineNumberAreaWidth: margin = {}'.format(self.lineNumberAreaWidth()))
        self.setViewportMargins(self.lineNumberAreaWidth(), 0, 0, 0)


    def updateLineNumberArea(self, rect, dy):
        print('CodeEditor.updateLineNumberArea: rect = {}, dy = {}'.format(rect, dy))

        if dy:
            self.lineNumberArea.scroll(0, dy)
        else:
            self.lineNumberArea.update(0, rect.y(), self.lineNumberArea.width(),
                                       rect.height())

        print('CodeEditor.updateLineNumberArea: rect.contains(self.viewport().rect()) = {}'.format(rect.contains(self.viewport().rect())))
        if rect.contains(self.viewport().rect()):
            self.updateLineNumberAreaWidth(0)


    def resizeEvent(self, event):
        super().resizeEvent(event)

        cr = self.contentsRect();
        self.lineNumberArea.setGeometry(QRect(cr.left(), cr.top(),
                                        self.lineNumberAreaWidth(), cr.height()))

    def lineNumberAreaPaintEvent(self, event):
        print('CodeEditor.lineNumberAreaPaintEvent')
        painter(self.lineNumberArea)
        painter.fillRect(event.rect(), Qt.lightGray)

        block = self.firstVisibleBlock()
        blockNumber = block.blockNumber()
        top = self.blockBoundingGeometry(block).translated(self.contentOffset()).top()
        bottom = top + self.blockBoundingRect(block).height()

        # Just to make sure I use the right font
        height = QFontMetrics(self.font()).height()
        while block.isValid() and (top <= event.rect().bottom()):
            if block.isVisible() and (bottom >= event.rect().top()):
                number = str(blockNumber + 1)
                painter.setPen(Qt.black)
                painter.drawText(0, top, lineNumberArea.width(), height,
                                 Qt.AlignRight, number)

            block = block.next()
            top = bottom
            bottom = top + self.blockBoundingRect(block).height()
            blockNumber += 1


    def highlightCurrentLine(self):
        extraSelections = []

        if not self.isReadOnly():
            selection = QTextEdit.ExtraSelection()

            lineColor = QColor(Qt.yellow).lighter(160)

            selection.format.setBackground(lineColor)
            selection.format.setProperty(QTextFormat.FullWidthSelection, True)
            selection.cursor = self.textCursor()
            selection.cursor.clearSelection()
            extraSelections.append(selection)
        self.setExtraSelections(extraSelections)


if __name__ == "__main__":
    app = QApplication(sys.argv)

    txt = CodeEditor()
    txt.show()

    sys.exit(app.exec_())

Any help would be greatly appreciated.

In case it matters: python: 3.4.3, PyQt: 4.8.6, OS: RHEL 6

magu_
  • 4,766
  • 3
  • 45
  • 79

1 Answers1

3

Your example looks mostly right. The issue with the missing paint events is caused by not setting a parent on the LineNumberArea widget. So you just need:

class LineNumberArea(QWidget):
    def __init__(self, editor):
        super().__init__(editor)

Also, the lineNumberAreaPaintEvent method has a few problems, but they're easily fixed:

def lineNumberAreaPaintEvent(self, event):
    # missing constructor
    painter = QPainter(self.lineNumberArea)
    ...
    # no need to use QFontMetrics
    height = self.fontMetrics().height()
    ...
            # missing self
            painter.drawText(0, top, self.lineNumberArea.width(), height,
                             Qt.AlignRight, number)

I'm not sure what you're trying to do with your implementation of lineNumberAreaWidth, as it doesn't seem to give the right results. The original implementation works exactly as expected, though (and is also much faster):

def lineNumberAreaWidth(self):
    digits = 1
    count = max(1, self.blockCount())
    while count >= 10:
        count /= 10
        digits += 1
    space = 3 + self.fontMetrics().width('9') * digits
    return space
ekhumoro
  • 115,249
  • 20
  • 229
  • 336