Text editor with tagged line numbers based on QPlainTextEdit


Referenceqt example
key code
CodeEditor.h file

#ifndef CODEEDITOR_H
#define CODEEDITOR_H

#include <QPlainTextEdit>
#include <QPaintEvent>
#include <QContextMenuEvent>
#include <QMouseEvent>
#include <QMouseEvent>
#include <QPainter>
#include <QTextBlock>
#include <QFontMetrics>
#include <QTextCursor>
#include <QTextBlockFormat>
#include <QTextDocument>
#include <QTextCodec>
#include <QMenu>
#include <QScrollBar>
QString GetQString(QByteArray byteArray);//Prevent garbled characters
enum TagType
{<!-- -->
    NUMBER=0,
    MARKROUND,//circle
    MARKBLOCK //block
};

class TagLineNumberArea;
class TagDataItem
{<!-- -->
public:
    TagDataItem(TagType tagTypeTmp, int widthTmp = 20):
       tagType(tagTypeTmp), width(widthTmp)
    {<!-- -->
    }
    bool BLineNumbersMark(int number)
    {<!-- -->
        //return std::find(vecLineNumbersMark.begin(), vecLineNumbersMark.end(),number) != vecLineNumbersMark.end();
        return mapMarkLineColor.find(number) != mapMarkLineColor.end();
    }
    void markerAdd(int line, QColor color=Qt::black)
    {<!-- -->
        mapMarkLineColor[line] = color;
    }
    QColor getLineColor(int line)
    {<!-- -->
        return mapMarkLineColor[line];
    }
    TagType tagType;
    int positionX;
    int width;
private:
    std::map<int, QColor> mapMarkLineColor;
};


class CodeEditor:public QPlainTextEdit
{<!-- -->
    Q_OBJECT
public:
    CodeEditor(QWidget *paren=0);
    ~CodeEditor();

    virtual void PaintTag(QPainter & amp; painter, int line, int y);

    void AddTag(TagDataItem* item);//Add tag or line number

    void UpdateColor(int line, int startIndex, int endIndex, QColor fontColor, QColor backColor);
    void UpdateColor(int startLine, int endLine, int startColumn, int endColumn, QColor fontColor, QColor backColor);//Update row font or background color

    void UpdateLineNumberWidth(int lineNumberWidthNew);//Update line number width
    void UpdateTagWidth(int lineNumberWidthOld, int lineNumberWidth);//Update the width behind the line number

    void GoToLine(int line);//Jump to line

    int GetLineNumber(QTextCursor & amp;cursor);//Get the line number

    void lineNumberAreaPaintEvent(QPaintEvent* event);//Drawing
    int lineNumberAreaWidth();//Calculate line number width
protected:
    virtual void resizeEvent(QResizeEvent *event)override;
private slots:
    void updateLineNumberAreaWidth(int newBlockCount);
    void highlightCurrentLine();
    void updateLineNumberArea(const QRect &rect, int dy);
    void updateLineNumber();
public:
    TagLineNumberArea *tagLineNumberArea;
    int currentLineNumber;
    int currentLinePositionY;

    std::vector<TagDataItem*> vecTagDataItem;
    QColor backgroundColor;
    QColor fontColor;
    QColor fontCurrentColor;
    int lineNumberPositionX;
    int lineNumberWidth;
    int tagCurrentWidth;
    int spacing;
    bool bLineNumber;
    bool bHightCurrentLineNumber;
};

class TagLineNumberArea: public QWidget
{<!-- -->
public:
    TagLineNumberArea(CodeEditor *editor):QWidget(editor), codeEditor(editor)
    {<!-- -->

    }

    QSize sizeHint()const override
    {<!-- -->
        return QSize(codeEditor->lineNumberAreaWidth(), 0);
    }
protected:
    void paintEvent(QPaintEvent* event)override
    {<!-- -->
        codeEditor->lineNumberAreaPaintEvent(event);
    }
public:
    CodeEditor* codeEditor;
};

#endif // CODEEDITOR_H

CodeEditor.cpp file

#include "CodeEditor.h"

CodeEditor::CodeEditor(QWidget *parent):QPlainTextEdit(parent)
{<!-- -->
    setMouseTracking(true);//Get mouse movement
    setWordWrapMode(QTextOption::NoWrap);//Automatically wrap lines, which will cause problems with the obtained line numbers.
    backgroundColor = QColor(240,240,240);
    fontColor = QColor(160,160,160);
    fontCurrentColor = QColor(0,0,0);
    lineNumberPositionX = 0;
    tagCurrentWidth = 0;
    lineNumberWidth = 0;
    spacing = 5;
    bLineNumber = false;
    bHightCurrentLineNumber = true;

    tagLineNumberArea = new TagLineNumberArea(this);

    connect(this, & amp;CodeEditor::blockCountChanged, this, & amp;CodeEditor::updateLineNumberAreaWidth);
    connect(this, & amp;CodeEditor::updateRequest, this, & amp;CodeEditor::updateLineNumberArea);

    //connect(this, & amp;CodeEditor::cursorPositionChanged, this, & amp;CodeEditor::hightlightCurrentLine);

    UpdateLineNumberWidth(0);
}

CodeEditor::~CodeEditor()
{<!-- -->

}

void CodeEditor::PaintTag(QPainter & amp;painter, int line, int y)
{<!-- -->
    for (TagDataItem* item:vecTagDataItem)
    {<!-- -->
        switch (item->tagType)
        {<!-- -->
            case NUMBER:
                break;
            case MARKROUND:
                if(item->BLineNumbersMark(line))
                {<!-- -->
                    painter.setPen(QColor(item->getLineColor(line)));
                    painter.setBrush(QColor(item->getLineColor(line)));
                    int w = item->width < fontMetrics().height() ? item->width : fontMetrics().height();
                    painter.drawEllipse(item->positionX + item->width/2 - w/2, y + fontMetrics().height()/2 - w/2,
                                        w, w);
                }
                break;
            case MARKBLOCK:
                if(item->BLineNumbersMark(line))
                {<!-- -->
                    painter.setPen(QColor(item->getLineColor(line)));
                    painter.setBrush(QColor(item->getLineColor(line)));
                    painter.drawRect(item->positionX, y, item->width, fontMetrics().height());
                }
                break;
            default:
                break;
        }
    }
}


void CodeEditor::AddTag(TagDataItem *item)
{<!-- -->
    item->positionX = tagCurrentWidth + spacing;
    vecTagDataItem.push_back(item);
    tagCurrentWidth = tagCurrentWidth + spacing + item->width;
    if(!bLineNumber & amp; & amp; item->tagType == NUMBER)
    {<!-- -->
        lineNumberPositionX = item->positionX;
        lineNumberWidth = item->width;
        bLineNumber = true;
    }
}

void CodeEditor::UpdateColor(int line, int startIndex, int endIndex, QColor fontColor, QColor backColor)
{<!-- -->
    QTextCursor cursor = this->textCursor();

    //Move row
    cursor.movePosition(QTextCursor::Start);
    for(int i=1; i<line; i + + )
    {<!-- -->
        cursor.movePosition(QTextCursor::Down);
    }
    //move column
    cursor.movePosition(QTextCursor::StartOfBlock);
    for(int i=1; i<startIndex; i + + )
    {<!-- -->
        cursor.movePosition(QTextCursor::NextCharacter);
    }
    if(endIndex == 0)
    {<!-- -->
        cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
    }
    else
    {<!-- -->
        for(int i=1; i<endIndex; i + + )
        {<!-- -->
            cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor);
        }
    }
    QTextCharFormat fmt;
    fmt.setForeground(QBrush(fontColor));//Font color
    fmt.setBackground(QBrush(backColor));//Background color
    cursor.setCharFormat(fmt);
}

void CodeEditor::UpdateColor(int startLine, int endLine, int startColumn, int endColumn, QColor fontColor, QColor backColor)
{<!-- -->
    QTextCursor cursor = this->textCursor();

    //Move row
    cursor.movePosition(QTextCursor::Start);
    for(int i=1; i<startLine; i + + )
    {<!-- -->
        cursor.movePosition(QTextCursor::Down);
    }
    //move column
    cursor.movePosition(QTextCursor::StartOfBlock);
    for(int i=1; i<startColumn; i + + )
    {<!-- -->
        cursor.movePosition(QTextCursor::NextCharacter);
    }
    //Move row
    cursor.movePosition(QTextCursor::Start);
    for(int i=startLine; i<endLine; i + + )
    {<!-- -->
        cursor.movePosition(QTextCursor::Down, QTextCursor::KeepAnchor);
    }
    //move column
    cursor.movePosition(QTextCursor::StartOfBlock, QTextCursor::KeepAnchor);
    for(int i=0; i<endColumn; i + + )
    {<!-- -->
        cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor);
    }

    QTextCharFormat fmt;
    fmt.setForeground(QBrush(fontColor));//Font color
    fmt.setBackground(QBrush(backColor));//Background color
    cursor.setCharFormat(fmt);
}

void CodeEditor::UpdateLineNumberWidth(int lineNumberWidthNew)
{<!-- -->
    int width = tagCurrentWidth - lineNumberWidth + lineNumberWidthNew;
    tagCurrentWidth = width;
    UpdateTagWidth(lineNumberWidth, lineNumberWidthNew);
    lineNumberWidth = lineNumberWidthNew;
}

void CodeEditor::UpdateTagWidth(int lineNumberWidthOld, int lineNumberWidth)
{<!-- -->
    bool numberAfterTag = false;
    for(TagDataItem* item: vecTagDataItem)
    {<!-- -->
        if(item->tagType == NUMBER)
        {<!-- -->
            numberAfterTag = true;
        }

        if(numberAfterTag)
        {<!-- -->
            item->positionX = item->positionX - lineNumberWidthOld + lineNumberWidth;
        }
    }
}

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

void CodeEditor::hightlightCurrentLine()
{<!-- -->
    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);
}

void CodeEditor::updateLineNumberArea(const QRect &rect, int dy)
{<!-- -->
    if(dy)
        tagLineNumberArea->scroll(0, dy);
    else
        tagLineNumberArea->update(0, rect.y(), tagLineNumberArea->width(), rect.height());

    if (rect.contains(viewport()->rect()))
        updateLineNumberAreaWidth(0);
}

void CodeEditor::updateLineNumber()
{<!-- -->
    tagLineNumberArea->update();
}

void CodeEditor::GoToLine(int line)
{<!-- -->
    QTextCursor textCursor = this->textCursor();
    int position = document()->findBlockByNumber(line - 1).position();
    textCursor.setPosition(position, QTextCursor::MoveAnchor);
    setTextCursor(textCursor);
    this->verticalScrollBar()->setValue(line-1);
}

int CodeEditor::GetLineNumber(QTextCursor & amp;cursor)
{<!-- -->
    QTextLayout *layout = cursor.block().layout();
    int pos = cursor.position() - cursor.block().position();
    int line = layout->lineForTextPosition(pos).lineNumber() + cursor.block().firstLineNumber();
    return line;
}

int CodeEditor::lineNumberAreaWidth()
{<!-- -->
    if(!bLineNumber)
        return tagCurrentWidth;
    int digits = 1;
    int max = qMax(1, blockCount());
    while(max >= 10)
    {<!-- -->
        max /=10;
         + + digits;
    }
    int lineNumberWidth = 3 + fontMetrics().horizontalAdvance(QLatin1Char('9'))*digits;
    UpdateLineNumberWidth(lineNumberWidth);
    return tagCurrentWidth;
}

void CodeEditor::lineNumberAreaPaintEvent(QPaintEvent *event)
{<!-- -->
   //Current row
    QTextCursor cursor = textCursor();
    int line = GetLineNumber(cursor);
    currentLineNumber = line;
    currentLinePositionY = -1;
    //background
    QPainter painter(tagLineNumberArea);
    painter.setRenderHint(QPainter::Antialiasing, true);
    painter.fillRect(event->rect(), backgroundColor);

    //
    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() & amp; & amp; top <= event->rect().bottom())
    {<!-- -->
        if(block.isVisible() & amp; & amp; bottom >= event->rect().top())
        {<!-- -->
            QString number = QString::number(blockNumber + 1);
            if(blockNumber == line)
                currentLinePositionY = top;//Record the current position
            //Record the line number
            if(bLineNumber)
            {<!-- -->
                if(blockNumber == line & amp; & amp; bHightCurrentLineNumber)
                {<!-- -->
                    painter.setPen(fontCurrentColor);//Highlight the current line number
                    QFont ft = painter.font();
                    ft.setBold(true);
                    painter.setFont(ft);
                }
                else
                {<!-- -->
                    painter.setPen(fontColor);
                }
                painter.drawText(lineNumberPositionX, top, lineNumberWidth, fontMetrics().height(),
                                 Qt::AlignRight, number);
            }
            //Draw tag
            PaintTag(painter, blockNumber, top);
        }
        block = block.next();
        top = bottom;
        bottom = top + qRound(blockBoundingGeometry(block).height());
         + + blockNumber;
    }
}

void CodeEditor::resizeEvent(QResizeEvent *e)
{<!-- -->
    QPlainTextEdit::resizeEvent(e);

    QRect cr = contentsRect();
    tagLineNumberArea->setGeometry(QRect(cr.left(), cr.top(), lineNumberAreaWidth(), cr.height()));
}

QString GetQString(QByteArray byteArray)
{<!-- -->
    QTextCodec::ConverterState state;
    QTextCodec *codec = QTextCodec::codecForName("UTF-8");
    QString qstr = codec->toUnicode(byteArray.constData(), byteArray.size(), & amp;state);
    if(state.invalidChars > 0)
        qstr = QTextCodec::codecForName("GBK")->toUnicode(byteArray);
    else {<!-- -->
        qstr = byteArray;
    }
    return qstr;
}

mainwindow.cpp

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "CodeEditor.h"
#include <QDebug>

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent)
{<!-- -->
    CodeEditor *edit = new CodeEditor;

    TagDataItem* item = new TagDataItem(MARKBLOCK, 10);
    item->markerAdd(2, Qt::blue);
    item->markerAdd(5, Qt::yellow);
    item->markerAdd(526, Qt::red);
    item->markerAdd(527, Qt::black);
    edit->AddTag(item);

    TagDataItem *itemNumber = new TagDataItem(NUMBER, 20);
    edit->AddTag(itemNumber);

    TagDataItem *item2 = new TagDataItem(MARKBLOCK, 10);
    item2->markerAdd(5, Qt::blue);
    item2->markerAdd(6, Qt::yellow);
    item2->markerAdd(7, Qt::red);
    item2->markerAdd(8, Qt::red);
    item2->markerAdd(526, Qt::red);
    item2->markerAdd(527, Qt::red);
    edit->AddTag(item2);

    this->setCentralWidget(edit);
    QFont ft;
    ft.setFamily("Microsoft Yahei");
    ft.setPointSize(10);
    edit->setFont(ft);

    QFile file("C:/Data/test.cpp");
    qDebug() << file.exists();
    if(file.open(QIODevice::ReadOnly))
    {<!-- -->
        edit->appendPlainText(file.readAll());
        file.close();
    }
    edit->GoToLine(520);

    edit->UpdateColor(1,3,2, 5,Qt::red, Qt::yellow);
    edit->UpdateColor(2,3,2, 5,Qt::red, Qt::yellow);

}

MainWindow::~MainWindow()
{<!-- -->
}