用Qt写一个简单的代码编辑器
关注、星标公众号,直达精彩内容
来源:技术让梦想更伟大
作者:李肖遥
这次的代码编辑器比较简单,主要有以下几个功能:
简单编辑
显示行号
突出显示当前行
如下图所示,主要来看看怎么实现。
代码编辑器的实现
代码编辑器主要是使用了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;
}
注意,除了检查该块是否在视口区域之外,我们还要检查该块是否可见。
扩展
除了行号之外,还可以在额外的区域中添加更多内容,例如,断点等等,本文是一个入门级别的编译器,后续会增加更多功能
嵌入式编程专辑 Linux 学习专辑 C/C++编程专辑 Qt进阶学习专辑
关注我的微信公众号,回复“加群”按规则加入技术交流群。
点击“阅读原文”查看更多分享。