首先看最终效果:
主要要实现的地方是行号的显示,还有选中行的高亮。
项目结构
整个程序只有三个文件,最主要的只有一个CodeEditor类,它是继承自QPlainTextEdit,这个类相比于普通的TextEdit更适合于做富 文本编辑器。
头文件
//codeeditor.h #ifndef CODEEDITOR_H #define CODEEDITOR_H #include <QPlainTextEdit> #include <QObject> QT_BEGIN_NAMESPACE class QPaintEvent; class QResizeEvent; class QSize; class QWidget; QT_END_NAMESPACE class LineNumberArea; //![codeeditordefinition] 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:LineNumberArea *lineNumberArea;};//![codeeditordefinition]//![extraarea]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:
Code Editor *codeEditor;};//![extraarea]#endif
头文件中包含了两个类的定义:继承QPlainTextEdit的CodeEditor和继承QWidget的LineNumberArea。写在一起的原因:在显示行号的时候需要用到QPlainTextEdit的protected方法,为了方便就直接写死在CodeEditor中。
在编辑器中,当代码的行数发生改变,或者编辑器的大小发生改变,行号都需要重新绘制,为此而创建了两个slot:updateLineNumberWidth() 和 updateLineNumberArea()。
cpp文件
//codeeditor.cpp #include <QtWidgets> #include "codeeditor.h" //![constructor] 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(); } //![constructor] //![extraAreaWidth] 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; } //![extraAreaWidth] //![slotUpdateExtraAreaWidth] void CodeEditor::updateLineNumberAreaWidth(int /* newBlockCount */) { setViewportMargins(lineNumberAreaWidth(), 0, 0, 0); } //![slotUpdateExtraAreaWidth] //![slotUpdateRequest] 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); } //![slotUpdateRequest] //![resizeEvent] void CodeEditor::resizeEvent(QResizeEvent *e) { QPlainTextEdit::resizeEvent(e); QRect cr = contentsRect(); lineNumberArea->setGeometry(QRect(cr.left(), cr.top(), lineNumberAreaWidth(), cr.height())); } //![resizeEvent] //![cursorPositionChanged] 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); } //![cursorPositionChanged] //![extraAreaPaintEvent_0] void CodeEditor::lineNumberAreaPaintEvent(QPaintEvent *event) { QPainter painter(lineNumberArea); painter.fillRect(event->rect(), Qt::lightGray); //![extraAreaPaintEvent_0] //![extraAreaPaintEvent_1] QTextBlock block = firstVisibleBlock(); int blockNumber = block.blockNumber(); int top = (int) blockBoundingGeometry(block).translated(contentOffset()).top(); int bottom = top + (int) blockBoundingRect(block).height(); //![extraAreaPaintEvent_1] //![extraAreaPaintEvent_2] 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; } } //![extraAreaPaintEvent_2]
在cpp中,首先是类的构造函数,初始化私有成员,链接相应的signal和slot。计算linenumbeearea的宽度并高亮第一行也是必须的。lineNumberAreaWidth():用于计算 LineNumberArea的宽度,取得最大行数,将其位数与数字9的宽度相乘,再加上3的间隔就可以了。
updateLineNumberAreaWidth(int):更新LineNumberArea的宽度,主要用到了setViewportMargins(),设置内边距。当行数不断增加,只要将左边的内边距加大就可以了。
updateLineNumberArea(const QRect &rect, int dy):当有换行出现的时候,对LineNumberArea进行更新
resizeEvent(QResizeEvent *e):当窗口大小变化的时候,LineNumberArea的大小也要进行变化。
highlightCurrentLine():当光标位置发生改变的时候,高亮的位置也发生改变。
lineNumberAreaPaintEvent(QPaintEvent *event):主要负责显示行号,首先是绘制黑色的背景,然后循环绘制行号。在plain text edit中,每一行都只包含一个 QTextBlock。
因此,出现折行的话也不用担心行号不正确。注意要进行两次判断,一次是calid,依次是visible。