很可惜,qt的几个编辑框并没有相关功能。所以我们要自己实现一个。
先讲讲原理:
QPlainTextEdit继承自QAbstractScrollArea,编辑发生在其viewport()的边距内。我们可以通过将视口的左边缘设置一个空白区域,用于绘制行号。
之所以使用QPlainTextEdit而不是QTextEdit,因为它针对处理纯文本进行了优化。更重要的是它允许我们在高亮多行文字。
一.LineNumberArea类
我们新建一个类,叫做LineNumberArea,继承QWidget,并在上绘制行号。
class LineNumberArea : public QWidget
{
public:
LineNumberArea(CodeEditor *editor) : QWidget(editor), codeEditor(editor)
{}
QSize sizeHint() const override
{
return QSize(codeEditor->lineNumberAreaWidth(), 0);
}
protected:
void paintEvent(QPaintEvent *event) override
{
codeEditor->lineNumberAreaPaintEvent(event);
}
private:
CodeEditor *codeEditor;
};
二.CodeEditor类
CodeEditor继承QPlainTextEdit,LineNumberArea是他的私有成员变量。
此外我们还要增加几个方法,用于计算左侧行号的渲染绘制的空白区域大小。
详见代码
class CodeEditor : public QPlainTextEdit
{
Q_OBJECT
public:
CodeEditor(QWidget *parent = nullptr);
void lineNumberAreaPaintEvent(QPaintEvent *event);//响应绘制事件
int lineNumberAreaWidth();//计算行号宽度
protected:
void resizeEvent(QResizeEvent *event) override;//响应窗口尺寸改变事件
private slots:
void updateLineNumberAreaWidth(int newBlockCount);//更新行号宽度
void highlightCurrentLine();//高亮当前行
void updateLineNumberArea(const QRect &rect, int dy);//更新行号区域
private:
QWidget *lineNumberArea;
};
当编辑器中的行数发生变化或者编辑器的viewport()滚动时或者编辑器的大小发生时,我们需要调整左侧区域大小,并绘制行号。因此我们需要updateLineNumberWidth()和updateLineNumberArea()方法。
三.CodeEditor类实现
在构造函数中,我们将QPlainTextEdit中的信号连接到对应的槽函数。
然后计算行号区域宽度并高亮显示第一行。
CodeEditor::CodeEditor(QWidget *parent) : QPlainTextEdit(parent)
{
lineNumberArea = new LineNumberArea(this);
connect(this, &CodeEditor::blockCountChanged, this, &CodeEditor::updateLineNumberAreaWidth);
connect(this, &CodeEditor::updateRequest, this, &CodeEditor::updateLineNumberArea);
connect(this, &CodeEditor::cursorPositionChanged, this, &CodeEditor::highlightCurrentLine);
updateLineNumberAreaWidth(0);//计算行号区域宽度
highlightCurrentLine();//高亮显示
}
lineNumberAreaWidth()函数计算LineNumberArea的宽度并返回。
一般取编辑器最后一行的文字,并计算该行文字的宽度。
按字符’9‘的宽度作为单个字符的宽度。
int CodeEditor::lineNumberAreaWidth()
{
int digits = 1;
int max = qMax(1, blockCount());
//计算数位
while (max >= 10) {
max /= 10;
++digits;
}
//取字符9的宽度
int space = 3 + fontMetrics().horizontalAdvance(QLatin1Char('9')) * digits;
return space;
}
当我们更新行号区域的宽度时,需调用QAbstractScrollArea::setViewportMargins(),更新设置左侧行号区的宽度。
void CodeEditor::updateLineNumberAreaWidth(int /* newBlockCount */)
{
setViewportMargins(lineNumberAreaWidth(), 0, 0, 0);
}
当编辑器视口滚动时,会调用updateLineNumberArea。QRect是编辑区域中需要更新(重绘)的部分。dy是鼠标滚动时的距离,单位是像素。
void CodeEditor::updateLineNumberArea(const QRect &rect, int dy)
{
if (dy)
lineNumberArea->scroll(0, dy);
else
lineNumberArea->update(0, rect.y(), lineNumberArea->width(), rect.height());
if (rect.contains(viewport()->rect()))
updateLineNumberAreaWidth(0);
}
当编辑器的大小发生变化时,我们还需要调整行号区域的大小。
void CodeEditor::resizeEvent(QResizeEvent *e)
{
QPlainTextEdit::resizeEvent(e);
QRect cr = contentsRect();
lineNumberArea->setGeometry(QRect(cr.left(), cr.top(), lineNumberAreaWidth(), cr.height()));
}
当光标位置改变时,我们突出显示当前行,即包含光标的行。
void CodeEditor::highlightCurrentLine()
{
QList<QTextEdit::ExtraSelection> extraSelections;
if (!isReadOnly()) {
QTextEdit::ExtraSelection selection;
QColor lineColor = QColor(Qt::yellow).lighter(160);
selection.format.setBackground(lineColor);
selection.format.setProperty(QTextFormat::FullWidthSelection, true);
selection.cursor = textCursor();
selection.cursor.clearSelection();
extraSelections.append(selection);
}
setExtraSelections(extraSelections);
}
每当LineNumberArea接收到绘制事件(paintEvent)时,就会调用CodeEditor的lineNumberAreaPaintEvent。
lineNumberAreaPaintEvent将遍历所有可见的线条,并在每行的额外区域中绘制行号。在纯文本编辑中,每一行都由一个QTextBlock组成;
void CodeEditor::lineNumberAreaPaintEvent(QPaintEvent *event)
{
QPainter painter(lineNumberArea);
painter.fillRect(event->rect(), Qt::lightGray);
QTextBlock block = firstVisibleBlock();
int blockNumber = block.blockNumber();
int top = qRound(blockBoundingGeometry(block).translated(contentOffset()).top());
int bottom = top + qRound(blockBoundingRect(block).height());
while (block.isValid() && top <= event->rect().bottom()) {
if (block.isVisible() && bottom >= event->rect().top()) {
QString number = QString::number(blockNumber + 1);
painter.setPen(Qt::black);
painter.drawText(0, top, lineNumberArea->width(), fontMetrics().height(),
Qt::AlignRight, number);
}
block = block.next();
top = bottom;
bottom = top + qRound(blockBoundingRect(block).height());
++blockNumber;
}
}
参考资料:
Code Editor Example | Qt Widgets 5.15.17