首先看最终效果:



主要要实现的地方是行号的显示,还有选中行的高亮。


项目结构


整个程序只有三个文件,最主要的只有一个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。