Pyside2 - 更改字体时代码编辑器中的行号不正确 family/size

Pyside2 - Linenumbers in codeeditor incorrect when changed font family/size

我从 Qt5 官方网站 https://doc.qt.io/qt-5/qtwidgets-widgets-codeeditor-example.html 查看了这个代码编辑器示例。它是用 C++ 编写的,但我使用 Pyside2 在 Python 中实现了它。

示例代码按原样运行良好,但是,当我尝试更改 QPlainTextEdit 的字体系列和大小时,事情开始变得混乱。我尝试调整许多不同的字段,例如使用 fontMetrics 来确定高度等

这是重现问题的最小示例

import sys
import signal
from PySide2.QtCore import Qt, QSize, QRect
from PySide2.QtGui import QPaintEvent, QPainter, QColor, QResizeEvent
from PySide2.QtWidgets import QWidget, QPlainTextEdit, QVBoxLayout
from PySide2 import QtCore
from PySide2.QtWidgets import QApplication


FONT_SIZE = 20
FONT_FAMILY = 'Source Code Pro'


class PlainTextEdit(QPlainTextEdit):
    def __init__(self, parent=None):
        super().__init__(parent)

        self.init_settings_font()

    def init_settings_font(self):
        font = self.document().defaultFont()

        font.setFamily(FONT_FAMILY)
        font.setFixedPitch(True)
        font.setPixelSize(FONT_SIZE)
        self.document().setDefaultFont(font)


class LineNumberArea(QWidget):
    TMP = dict()

    def __init__(self, editor):
        super().__init__(editor)
        self._editor = editor

        self._editor.blockCountChanged.connect(lambda new_count: self._update_margin())
        self._editor.updateRequest.connect(lambda rect, dy: self._update_request(rect, dy))

        self._update_margin()

    def width(self) -> int:
        # we use 1000 as a default size, so from 0-9999 this length will be applied
        _max = max(1000, self._editor.blockCount())
        digits = len(f'{_max}')
        space = self._editor.fontMetrics().horizontalAdvance('0', -1) * (digits + 1) + 6
        return QSize(space, 0).width()

    def _update_line_geometry(self):
        content_rect = self._editor.contentsRect()
        self._update_geometry(content_rect)

    def _update_geometry(self, content_rect: QRect):
        self.setGeometry(
            QRect(content_rect.left(), content_rect.top(), self.width(), content_rect.height())
        )

    def _update_margin(self):
        self._editor.setViewportMargins(self.width(), 0, 0, 0)

    def _update_request(self, rect: QRect, dy: int):
        self._update(0, rect.y(), self.width(), rect.height(), self._editor.contentsRect())

        if rect.contains(self._editor.viewport().rect()):
            self._update_margin()

    def _update(self, x: int, y: int, w: int, h: int, content_rect: QRect):
        self.update(x, y, w, h)
        self._update_geometry(content_rect)

    # override
    def resizeEvent(self, event: QResizeEvent) -> None:
        self._update_line_geometry()

    # override
    def paintEvent(self, event: QPaintEvent):
        painter = QPainter(self)
        area_color = QColor('darkgrey')

        # Clearing rect to update
        painter.fillRect(event.rect(), area_color)

        visible_block_num = self._editor.firstVisibleBlock().blockNumber()
        block = self._editor.document().findBlockByNumber(visible_block_num)
        top = self._editor.blockBoundingGeometry(block).translated(self._editor.contentOffset()).top()
        bottom = top + self._editor.blockBoundingRect(block).height()
        active_line_number = self._editor.textCursor().block().blockNumber() + 1

        # font_size = storage.get_setting(Constants.Editor_font_size).value
        font = self._editor.font()

        while block.isValid() and top <= event.rect().bottom():
            if block.isVisible() and bottom >= event.rect().top():
                number_to_draw = visible_block_num + 1

                if number_to_draw == active_line_number:
                    painter.setPen(QColor('black'))
                else:
                    painter.setPen(QColor('white'))

                font.setPixelSize(self._editor.document().defaultFont().pixelSize())
                painter.setFont(font)

                painter.drawText(
                    -5,
                    top,
                    self.width(),
                    self._editor.fontMetrics().height(),
                    int(Qt.AlignRight | Qt.AlignHCenter),
                    str(number_to_draw)
                )

            block = block.next()
            top = bottom
            bottom = top + self._editor.blockBoundingGeometry(block).height()
            visible_block_num += 1

        painter.end()

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

    signal.signal(signal.SIGINT, signal.SIG_DFL)

    window = QWidget()
    layout = QVBoxLayout()
    editor = PlainTextEdit()
    line_num = LineNumberArea(editor)

    layout.addWidget(editor)
    window.setLayout(layout)

    window.show()

    sys.exit(app.exec_())

最大的问题之一是明文出口似乎有一个顶部边距偏移量,我无法在行号小部件中动态获取它。 并且将编辑器字体设置为画家时,数字将不会绘制相同的大小!?

有谁知道如何将行号调整到与相应文本相同的水平水平,并以动态方式使它们的大小相同,这意味着如果将字体设置为其他字体,他们应该全部自动调整。

问题出在您出于不同目的使用两种字体:widget 字体和 document 字体。

每种字体都有不同的方面,如果您将这些字体视为绘图坐标的基础,其对齐方式可能会有所不同。

由于您使用文档字体绘图但使用小部件字体作为参考,结果是您将遇到绘图问题:

  • 即使磅值相同,也会在不同高度绘制不同的字体,尤其是当文本矩形的对齐方式不正确时(另请注意,您使用的对齐方式不一致,因为 Qt.AlignRight | Qt.AlignHCenter 将始终考虑右对齐,默认为顶部对齐)
  • 您正在使用 小部件 字体规格来设置文本矩形高度,这与文档的规格不同,因此您将限制绘图的高度。

与其他小部件不同,Qt 中的富文本编辑器有两种字体设置:

  • 小部件字体;
  • (默认)文档字体,可以被文档中的 QTextOption 覆盖;

文档会总是默认继承widget字体(或应用程序字体),在运行时为widget设置字体时也会出现这种情况,甚至对于应用程序(除非已为小部件 显式设置字体 )。

为编辑器设置字体通常适用于简单的情况,但您必须记住字体会传播,因此子控件也会继承字体。

另一方面,为文档设置默认字体不会传播到子项,但是,如上所述,如果在运行时发生更改,应用程序字体可能会覆盖它。

对于您的情况,最简单的解决方案是为编辑器小部件而不是文档设置字体。通过这种方式,您可以确保 LineNumberArea(编辑器的子项)也将继承相同的字体。使用这种方法,您甚至不需要设置画家的字体,因为它将始终使用小部件字体。

如果您想使用不同的字体并仍然保持正确对齐,则必须考虑用于文档的字体的基线位置,并将该参考用作小部件字体的基线。为此,您必须使用两个字体指标的 ascent() 之差来转换块位置。