用Qt写一个简单的代码编辑器

李肖遥

共 7683字,需浏览 16分钟

 · 2021-05-29

关注、星标公众号,直达精彩内容

来源:技术让梦想更伟大

作者:李肖遥


这次的代码编辑器比较简单,主要有以下几个功能:

  • 简单编辑

  • 显示行号

  • 突出显示当前行

如下图所示,主要来看看怎么实现。

代码编辑器的实现

代码编辑器主要是使用了CodeEditor和LineNumberArea,其实现步骤如下:

CodeEditor是继承QPlainTextEdit的小部件,在CodeEditor(LineNumberArea)中保留一个单独的小部件,在其上绘制行号。

QPlainTextEdit继承自QAbstractScrollArea,并且编辑在其viewport()的边距内进行。通过将视口的左边距设置为绘制行号所需的尺寸,为行号区域腾出空间。

在编辑代码时,我们首选QPlainTextEdit而不是QTextEdit,因为它已针对处理纯文本进行了优化。

除了用户可以使用鼠标或键盘进行的选择之外,QPlainTextEdit还允许我们添加选择,我们使用此功能突出显示当前行。

LineNumberArea类

在此部件上绘制行号,并将其放置在CodeEditor的viewport()的左边距区域上,QWidget类也可以帮助我们对其内容进行滚动。另外,如果使用断点或其他代码编辑器功能扩展编辑器,单独的窗口小部件是正确的选择。

class LineNumberArea : public QWidget
{
public:
    LineNumberArea(CodeEditor *editor) : QWidget(editor) {
        codeEditor = editor;
    }

    QSize sizeHint() const {
        return QSize(codeEditor->lineNumberAreaWidth(), 0);
    }

protected:
    void paintEvent(QPaintEvent *event) {
        codeEditor->lineNumberAreaPaintEvent(event);
    }

private:
    CodeEditor *codeEditor;
};

CodeEditor类定义

这是代码编辑器的类定义:

class CodeEditor : public QPlainTextEdit
{
    Q_OBJECT

public:
    CodeEditor(QWidget *parent = 0);

    void lineNumberAreaPaintEvent(QPaintEvent *event);
    int lineNumberAreaWidth();

protected:
    void resizeEvent(QResizeEvent *event);

private slots:
    void updateLineNumberAreaWidth(int newBlockCount);
    void highlightCurrentLine();
    void updateLineNumberArea(const QRect &, int);

private:
    QWidget *lineNumberArea;
};

在编辑器中,调整大小并在LineNumberArea上绘制行号,当编辑器中的行数更改以及滚动编辑器的viewport()时,执行此操作,每当光标的位置发生变化时,会在highlightCurrentLine()中突出显示当前行。

CodeEditor类的实现

现在,我们将从构造函数开始,通过代码编辑器实现。

CodeEditor::CodeEditor(QWidget *parent) : QPlainTextEdit(parent)
{
    lineNumberArea = new LineNumberArea(this);

    connect(this, SIGNAL(blockCountChanged(int)), this, SLOT(updateLineNumberAreaWidth(int)));
    connect(this, SIGNAL(updateRequest(QRect,int)), this, SLOT(updateLineNumberArea(QRect,int)));
    connect(this, SIGNAL(cursorPositionChanged()), this, SLOT(highlightCurrentLine()));

    updateLineNumberAreaWidth(0);
    highlightCurrentLine();
}

在构造函数中,我们将插槽连接到QPlainTextEdit中的信号,创建编辑器时,必须计算行号区域的宽度并突出显示第一行。

int CodeEditor::lineNumberAreaWidth()
{
    int digits = 1;
    int max = qMax(1, blockCount());
    while (max >= 10) {
        max /= 10;
        ++digits;
    }

    int space = 3 + fontMetrics().width(QLatin1Char('9')) * digits;

    return space;
}

lineNumberAreaWidth()函数计算LineNumberArea小部件的宽度,在编辑器的最后一行取位数,然后将其乘以位数的最大宽度。

void CodeEditor::updateLineNumberAreaWidth(int /* newBlockCount */)
{
    setViewportMargins(lineNumberAreaWidth(), 0, 0, 0);
}

当更新行号区域的宽度时,我们只需调用QAbstractScrollArea::setViewportMargins()。

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);
}

当光标位置更改时,我们突出显示当前行,即包含光标的行,在此之前需要先清除光标选择。

使用文本光标设置选择,使用FullWidthSelection属性时,将选择当前光标文本块,并根据设置的位置移动光标。

void CodeEditor::lineNumberAreaPaintEvent(QPaintEvent *event)
{
    QPainter painter(lineNumberArea);
    painter.fillRect(event->rect(), Qt::lightGray);

每当LineNumberAreaPaintEvent()收到绘制事件时,就会从LineNumberArea调用它。函数如下

QTextBlock block = firstVisibleBlock();
int blockNumber = block.blockNumber();
int top = (int) blockBoundingGeometry(block).translated(contentOffset()).top();
int bottom = top + (int) blockBoundingRect(block).height();

遍历所有可见线号,在纯文本编辑中,每一行将包含一个QTextBlock,获得第一个文本块的顶部和底部y坐标,并在循环的每次迭代中通过当前文本块的高度调整这些值。

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 + (int) blockBoundingRect(block).height();
    ++blockNumber;
}

注意,除了检查该块是否在视口区域之外,我们还要检查该块是否可见。

扩展

除了行号之外,还可以在额外的区域中添加更多内容,例如,断点等等,本文是一个入门级别的编译器,后续会增加更多功能

‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧  END  ‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧

推荐阅读:


嵌入式编程专辑
Linux 学习专辑
C/C++编程专辑
Qt进阶学习专辑

关注我的微信公众号,回复“加群”按规则加入技术交流群。


点击“阅读原文”查看更多分享。

浏览 81
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报