Qml TextArea 的行号/行高

Line numbers/ line height for a Qml TextArea

我们想在基于 QtQuick 的应用程序中实现嵌入式代码编辑器。为了突出显示,我们使用基于 KSyntaxHighlightingQSyntaxHighlighter。我们找不到确定允许我们在代码旁边显示行号的行高和行间距的方法。支持动态换行也是一个很好的补充。

    Flickable {
            id: flickable
            flickableDirection: Flickable.VerticalFlick
            Layout.preferredWidth: parent.width
            Layout.maximumWidth: parent.width
            Layout.minimumHeight: 200
            Layout.fillHeight: true
            Layout.fillWidth: true

            boundsBehavior: Flickable.StopAtBounds
            clip: true
            ScrollBar.vertical: ScrollBar {
                width: 15
                active: true
                policy: ScrollBar.AlwaysOn
            }

            property int rowHeight: textArea.font.pixelSize+3
            property int marginsTop: 10
            property int marginsLeft: 4
            property int lineCountWidth: 40

            Column {
                id: lineNumbers
                anchors.left: parent.left
                anchors.leftMargin: flickable.marginsLeft
                anchors.topMargin:   flickable.marginsTop
                y:  flickable.marginsTop
                width: flickable.lineCountWidth

                function range(start, end) {
                    var rangeArray = new Array(end-start);
                    for(var i = 0; i < rangeArray.length; i++){
                        rangeArray[i] = start+i;
                    }
                    return rangeArray;
                }

                Repeater {
                    model: textArea.lineCount
                    delegate:
                    Label {
                        color: (!visualization.urdfPreviewIsOK && (index+1) === visualization.urdfPreviewErrorLine) ? "white" :  "#666"
                        font: textArea.font
                        width: parent.width
                        horizontalAlignment: Text.AlignRight
                        verticalAlignment: Text.AlignVCenter
                        height: flickable.rowHeight
                        renderType: Text.NativeRendering
                        text: index+1
                        background: Rectangle {
                            color: (!visualization.urdfPreviewIsOK && (index+1) === visualization.urdfPreviewErrorLine) ? "red" : "white"
                        }
                    }
                }
            }
            Rectangle {
                y: 4
                height: parent.height
                anchors.left: parent.left
                anchors.leftMargin: flickable.lineCountWidth + flickable.marginsLeft
                width: 1
                color: "#ddd"
            }

        TextArea.flickable: TextArea {
                id: textArea

                property bool differentFromSavedState: fileManager.textDifferentFromSaved

                text: fileManager.textTmpState
                textFormat: Qt.PlainText
                //dont wrap to allow for easy line annotation wrapMode: TextArea.Wrap
                focus: false
                selectByMouse: true
                leftPadding: flickable.marginsLeft+flickable.lineCountWidth
                rightPadding: flickable.marginsLeft
                topPadding: flickable.marginsTop
                bottomPadding: flickable.marginsTop

                background: Rectangle {
                    color: "white"
                    border.color: "green"
                    border.width: 1.5
                }

                Component.onCompleted: {
                    fileManager.textEdit = textArea.textDocument
                }

                onTextChanged: {
                    fileManager.textTmpState = text
                }

                function update()
                {
                    text = fileManager.textTmpState
                }
            }
        }

如您所见,我们使用 property int rowHeight: textArea.font.pixelSize+3 来猜测行高和行间距,但是一旦 DPI 或系统的其他属性发生变化,它就会中断。

TextArea 类型有两个属性 contentWidthcontentHeight 包含文本内容的大小。

因此,如果将高度除以行数(可以用 属性 lineCount 得到),您将得到一行的高度:

property int rowHeight: textArea.contentHeight / textArea.lineCount

但是,如果您计划在同一文档中使用多个行间距,则必须通过操作 QTextDocument:

来处理每一行
class LineManager: public QObject
{
    Q_OBJECT
    Q_PROPERTY(int lineCount READ lineCount NOTIFY lineCountChanged)
public:
    LineManager(): QObject(), document(nullptr)
    {}
    Q_INVOKABLE void setDocument(QQuickTextDocument* qdoc)
    {
        document = qdoc->textDocument();
        connect(document, &QTextDocument::blockCountChanged, this, &LineManager::lineCountChanged);
    }

    Q_INVOKABLE int lineCount() const
    {
        if (!document)
            return 0;
        return document->blockCount();
    }

    Q_INVOKABLE int height(int lineNumber) const
    {
        return int(document->documentLayout()->blockBoundingRect(document->findBlockByNumber(lineNumber)).height());
    }
signals:
    void lineCountChanged();
private:
    QTextDocument* document;
};
    LineManager* mgr = new LineManager();
    QQuickView *view = new QQuickView;
    view->rootContext()->setContextProperty("lineCounter", mgr);
    view->setSource(QUrl("qrc:/main.qml"));
    view->show();
Repeater {
    model: lineCounter.lineCount
    delegate:
        Label {
            color: "#666"
            font: textArea.font
            width: parent.width
            height: lineCounter.height(index)
            horizontalAlignment: Text.AlignRight
            verticalAlignment: Text.AlignVCenter
            renderType: Text.NativeRendering
            text: index+1
            background: Rectangle {
                border.color: "black"
            }
    }
}

我找到了一个只有 QML 的解决方案:

  1. 使用 TextEdit 而不是 TextArea 以避免行号和文本之间的对齐问题
  2. 使用 'ListView' 生成文本编辑的行号:

这是一个初步的解决方案:

    RowLayout {
        anchors.fill: parent
        ListView {
            Layout.preferredWidth: 30
            Layout.fillHeight: true
            model: textEdit.text.split(/\n/g)
            delegate: Text { text: index + 1 }
        }
        TextEdit {
            id: textEdit
            Layout.fillWidth: true
            Layout.fillHeight: true
        }
    }

ListView 有每行文本的完整副本。我们可以使用这个副本来计算行高(考虑到自动换行)。我们通过创建一个不可见的 Text 来做到这一点。我们可以通过向 TextEdit 添加 Flickable 并同步 ListViewTextEdit 之间的滚动来进一步改进答案:

这里有一个更完整的解决方案:

// NumberedTextEdit.qml

import QtQuick 2.12
import QtQuick.Controls 2.5

Item {
    property alias lineNumberFont: lineNumbers.textMetrics.font
    property color lineNumberBackground: "#e0e0e0"
    property color lineNumberColor: "black"
    property alias font: textEdit.font
    property alias text: textEdit.text
    property color textBackground: "white"
    property color textColor: "black"

    Rectangle {
        anchors.fill: parent

        color: textBackground

        ListView {
            id: lineNumbers
            property TextMetrics textMetrics: TextMetrics { text: "99999"; font: textEdit.font }
            model: textEdit.text.split(/\n/g)
            anchors.left: parent.left
            anchors.top: parent.top
            anchors.bottom: parent.bottom
            anchors.margins: 10
            width: textMetrics.boundingRect.width
            clip: true

            delegate: Rectangle {
                width: lineNumbers.width
                height: lineText.height
                color: lineNumberBackground
                Text {
                    id: lineNumber
                    anchors.horizontalCenter: parent.horizontalCenter
                    text: index + 1
                    color: lineNumberColor
                    font: textMetrics.font
                }

                Text {
                    id: lineText
                    width: flickable.width
                    text: modelData
                    font: textEdit.font
                    visible: false
                    wrapMode: Text.WordWrap
                }
            }
            onContentYChanged: {
                if (!moving) return
                flickable.contentY = contentY
            }
        }

        Item {
            anchors.left: lineNumbers.right
            anchors.right: parent.right
            anchors.top: parent.top
            anchors.bottom: parent.bottom
            anchors.margins: 10

            Flickable {
                id: flickable
                anchors.fill: parent
                clip: true
                contentWidth: textEdit.width
                contentHeight: textEdit.height
                TextEdit {
                    id: textEdit
                    width: flickable.width
                    color: textColor
                    wrapMode: Text.WordWrap
                }
                onContentYChanged: {
                    if (lineNumbers.moving) return
                    lineNumbers.contentY = contentY
                }
            }
        }
    }
}

我发现您可以使用 FontMetrics 查询行高,然后通过 Math.ceil(fontMetrics.lineSpacing) 获取真实高度 例如:

TextEdit {
    id: textArea

     FontMetrics {
         id: fontMetricsId
         font: textArea.font
     }

     Component.onCompleted: {
          console.log("Line spacing:" + Math.ceil(fontMetricsId.lineSpacing)
     }
}