Qt 로 만들자: Syntax Highlighter

Qt Creator 뿐만 아니라 요즘의 에디터들은 모두 Syntax Highlighting 기능을 가지고 있다. 이번에는 이 Syntax Highlighting 기능을 구현해 보자.

1. 요구사항

  • 주석, 키워드, 기호를 색깔별로 나타낸다.

2. 코드 작성

2.1 프로젝트 작성

  • 프로젝트 이름 : Syntax Highlighter
  • 메인클래스 이름 : MainWindow
  • 메인클래스 유형 : QMainWindow

2.2 헤더 분석(mainwindow.h)


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/** @file mainwindow.h
 */


#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>

#include <QtWidgets>

/**
 * @brief SyntaxHighliter 클래스
 */

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = 0);
    ~MainWindow();

private:
    QTextEdit *_plainText;          /// 원본 텍스트
    QTextEdit *_syntaxText;         /// 문법 강조된 텍스트
    QPushButton *_highlightButton;  /// 문법 강조 실행 버튼

    void initMenus();
    void initWidgets();
    QString nextToken(const QString &s, const int start, int *next);

private slots:
    void plainTextChanged();
    void syntaxHighlight();
};

#endif // MAINWINDOW_H


원본 텍스트를 위한 에디터와 문법강조된 텍스트를 위한 에디터가 각각 있고, 문법 강조를 실행하기 위한 버튼이 하나 있다.

23~24번째 줄: QTextEdit 는 Qt 에서 제공하는 텍스트 에디터 위젯이다. 일반 텍스트 뿐만 아니라 HTML4 의 일부를 지원한다.

2.3 소스 분석(mainwindow.cpp)

2.3.1 생성자와 소멸자


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
 * @brief MainWindow 생성자
 * @param parent 부모 위젯
 */

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
{
    initMenus();    // 메뉴 초기화
    initWidgets();  // 위젯 초기화
}

/**
 * @brief MainWindow 소멸자
 */

MainWindow::~MainWindow()
{

}


별 내용은 없다. 메뉴와 위젯을 초기화한다.

2.3.2 initMenus()


1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
 * @brief 메뉴를 초기화함
 */

void MainWindow::initMenus()
{
    // "파일" 메뉴 생성
    QMenu *fileMenu = new QMenu(tr("파일(&F)"));
    // "끝내기" 액션 추가
    fileMenu->addAction(tr("끝내기(&x)"), this, SLOT(close()),
                        QKeySequence::Quit);

    // "파일" 메뉴 추가
    menuBar()->addMenu(fileMenu);
}


"파일" 메뉴를 추가하고, 그 메뉴에 "끝내기" 액션을 추가한다.

2.3.3 initWidgets()


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/**
 * @brief 위젯 초기화
 */

void MainWindow::initWidgets()
{
    // 원본 텍스트용 위젯 생성
    _plainText = new QTextEdit(this);
    // rich text 받지 않음
    _plainText->setAcceptRichText(false);
    // 기본 글꼴 설정
    _plainText->setStyleSheet("font-family:\"Courier New\";font-size:10pt;");
    // 현재 글꼴 설정
    _plainText->setFontFamily("Courier New");
    _plainText->setFontPointSize(10);

    // 현재 글꼴의 메트릭스 얻음
    QFontMetrics fm(_plainText->currentFont());
    // 위젯 최소 크기 설정. 80 문자 폭. 4 는 위젯의 내부 여백
    _plainText->setMinimumWidth(fm.averageCharWidth() * 81 + 4 +
                       _plainText->verticalScrollBar()->sizeHint().width());

    connect(_plainText, SIGNAL(textChanged()), this, SLOT(plainTextChanged()));

    // 문법 강조된 텍스트용 위젯 생성
    _syntaxText = new QTextEdit(this);
    // 읽기 전용
    _syntaxText->setReadOnly(true);
    _syntaxText->setText(tr("문법 강조 버튼을 누르세요"));

    // 문법 강조 버튼 생성
    _highlightButton = new QPushButton(tr("문법 강조(&H)"), this);
    // 문법 강조 버튼 작동 불가능하게
    _highlightButton->setEnabled(false);
    connect(_highlightButton, SIGNAL(clicked(bool)),
            this, SLOT(syntaxHighlight()));

    QVBoxLayout *vboxLayout = new QVBoxLayout;
    vboxLayout->addWidget(_plainText);
    vboxLayout->addWidget(_syntaxText);
    vboxLayout->addWidget(_highlightButton);

    QWidget *w = new QWidget(this);
    w->setLayout(vboxLayout);

    setCentralWidget(w);

    // 내부 위젯 크기에 맞추어 크기 조절
    adjustSize();

    // 원본 텍스트 위젯의 최소 크기 0 으로 설정하여, 크기 축소 가능하게
    _plainText->setMinimumWidth(0);
}


9 번째 줄: QTextEdit::setAcceptRichText() 는 rich text 를 받아들일 것인지를 결정한다. 원본 텍스트용 에디터에서는 rich text 를 받아들이지 않는다.

11 번째 줄: QWidget::setStyleSheet() 는 해당 위젯의 스타일을 결정한다. QTextEdit 의 경우 모든 서식이 지워졌을 때, 기본 스타일을 지정한다. 따라서 스타일을 지정하지 않으면, 에디터의 모든 내용이 지워지면, setFontFamily() 등으로 설정한 속성이 모두 사라진다.

12~13 번째 줄: QTextEdit::setFontFamily() 는 현재 사용할 글꼴을 설정한다. QTextEdit::setFontPointSize() 는 포인트 단위로 글꼴의 크기를 설정한다.

17 번째 줄: QFontMetrics 는 글꼴의 메트릭스 정보를 다루는 클래스이다.

19 번째 줄: QFontMetrics::averageCharWidth() 는 글꼴의 평균 글자 폭을 알려준다. "Courier New" 는 고정폭 글꼴이므로, 모든 글자의 폭이 같다. 따라서 평균 글자 폭을 사용하여 글자 폭을 얻어도 무방하다.

27 번째 줄: QTextEdit::setReadOnly() 는 문서를 읽기 전용으로 할지를 결정한다.

28 번째 줄: QTextEdit::setText() 는 문서의 텍스트를 설정한다.

48~51 번째 줄: QWidget::adjustSize() 는 자식 위젯을 모두 나타낼 수 있을 정도로 위젯의 크기를 바꾼다. adjustSize() 의 기준은 자식 위젯들의 최소 크기로 보인다. 이에 따라, 19 번째 줄에서 QWidget::setMinimumWidth() 로 최소 크기를 설정하여, adjustSize() 를 하더라도 원본 텍스트 위젯이 지정한 크기 미만으로 작아지지 않도록 한다. 그런데 이렇게 하면, 사용자 역시 창의 크기를 더 줄일 수 없다. 따라서 setMinimumWidth(0) 을 호출하여 최소 크기 제한을 없앤다.

2.3.4 syntaxHighlight()


1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
 * @brief 문법 강조하기
 */

void MainWindow::syntaxHighlight()
{
    TokenParser parser(_plainText->toPlainText());
    QString html;
    QList<TokenAbstract *> tokenTypes;

    // 블럭 토큰 추가
    tokenTypes.append(new TokenBlock("\"", "\"", "green"));
    tokenTypes.append(new TokenBlock("'", "'", "green"));
    tokenTypes.append(new TokenBlock("/*", "*/", "green"));
    tokenTypes.append(new TokenBlock("//", "\n", "green"));


6 번째 줄: TokenParser 는 문자열로부터 토큰을 분리해 내기 위한 클래스이다. 이후에 알아본다. QTextEdit::toPlainText() 는 에디터의 내용을 일반 텍스트로 돌려준다.

9 번째 줄: TokenAbstract  는 토큰을 나타내기 위한 추상 클래스이다. 이후에 알아본다.

11~14 번째 줄: TokenBlock 은 블럭을 나타내는 토큰이다. 이후에 알아본다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
    QStringList keywords;

    // 키워드 추가
    keywords << "asm" << "auto"
             << "bool" << "break"
             << "case" << "catch" << "cdecl" << "char" << "class" << "const"
                << "const_cast" << "continue"
             << "default" << "delete" << "double" << "do" << "dynamic_cast"
             << "else" << "enum" << "explicit" << "extern"
             << "far" << "float" << "for" << "friend"
             << "goto"
             << "huge"
             << "if" << "interrupt" << "int"
             << "long"
             << "mutable"
             << "namespace" << "near" << "new"
             << "operator"
             << "pascal" << "private" << "protected" << "public"
             << "register" << "reinterpret_cast" << "return"
             << "short" << "signed" << "sizeof" << "static" << "static_cast"
                << "struct" << "switch"
             << "template" << "this" << "throw" << "try" << "typedef"
                << "typename"
             << "union" << "unsigned" << "using"
             << "virtual" << "void" << "volatile"
             << "while"
             << "yield";

    // 특수 상수 추가
    keywords << "true" << "false"
             << "TRUE" << "FALSE"
             << "NULL";

    foreach (QString keyword, keywords)
        tokenTypes.append(new TokenKeyword(keyword, "#808000"));

    // 전처리기 지시자 추가
    QStringList directives;

    directives  << "define"
                << "elif" << "else" << "endif" << "error"
                << "if" << "ifdef" << "ifndef" << "include"
                << "line"
                << "pragma"
                << "undef"
                << "warning";

    foreach (QString directive, directives)
        tokenTypes.append(new TokenDirective(directive, "blue", "#"));

    // 기호 추가
    QStringList ops;

    ops << ">" << "<" << "{" << "}" << "(" << ")" << "[" << "]" << "+" << "-"
        << ":" << "&" << "!" << "|" << "=" << "~" << "?" << "." << ";"
        << "," << "%" << "^" << "/" << "*";

    foreach (QString op, ops)
        tokenTypes.append(new TokenKeyword(op, "red"));


C/C++ 관련 토큰을 추가한다.

35 번째줄: TokenKeyword 는 문자+숫자+_ 로 이루어진 단어와 일반 기호를 나타내는 토큰이다. 이후에 알아본다.

49 번째줄: TokenDirective 는 전처리기 지시자를 나타내는 토큰이다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
    bool escaped = false;           // 탈출 문자 사용 여부
    TokenBlock *currentBlock = 0;   // 현재 블럭 토큰

    // 파싱
    while (parser.hasNext())
    {
        QString token = parser.next();

        if (!escaped)   // 탈출 문자가 사용되지 않았으면
        {
            TokenAbstract *tokenType;
            bool matched = false;

            // 토큰 확인
            foreach(tokenType, tokenTypes)
            {
                if (tokenType->matched(&token, &parser))
                {
                    matched = true;

                    break;
                }
            }


5 번째 줄: TokenParser::hasNext() 는 토큰이 남아 있는지 알려준다.

7 번째 줄: TokenParser::next() 는 다음 토큰을 돌려준다.

17 번째 줄: TokenAbstract::matched() 는 토큰이 일치하는지 알려준다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
            if (matched) // 토큰 일치하면
            {
                if (!currentBlock)  // 블럭 내부가 아니면
                {
                    token = tokenType->html();

                    // 블럭 토큰이면 현재 블럭 토큰 설정
                    if (tokenType->type() == TokenAbstract::Block)
                        currentBlock = static_cast<TokenBlock *>(tokenType);
                }
                else    // 블럭 내부이면
                {
                    // 또다른 블럭이면 블럭 시작 상태 해제
                    if (tokenType->type() == TokenAbstract::Block &&
                            tokenType != currentBlock)
                        static_cast<TokenBlock *>(tokenType)->reset();

                    // 현재 블럭이 끝났으면
                    if (currentBlock == tokenType && !currentBlock->inner())
                    {
                        token = tokenType->html();

                        // 현재 블럭 토큰 없음
                        currentBlock = 0;
                    }
                    else
                        token = TokenAbstract::plainToHtml(token);
                }
            }
            else
                token = TokenAbstract::plainToHtml(token);
        }


5 번째 줄: TokenAbstract::html() 은 토큰을 HTML 로 표현한다.

14 번째 줄: TokenAbstract::type() 은 토큰의 종류를 알려준다.

16 번째 줄: TokenBlock::reset() 은 블럭 시작 상태를 초기화한다.

19 번째 줄: TokenBlock::inner() 은 블럭 내부인지 알려준다.

27 번째 줄: TokenAbstract::plainToHtml() 은 주어진 토큰을 HTML 로 바꾸어 돌려준다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
        // 토큰 추가
        html.append(token);

        // 탈출 문자 ?
        escaped = !escaped && token == "\\";
    }

    // 추가된 토큰 해제
    qDeleteAll(tokenTypes);

    // HTML 전체 글꼴 설정
    html.prepend("<div style=\"font-family:Courier New;font-size:10pt;\">");
    html.append("</div>");

    // 문법 강조용 텍스트 설정
    _syntaxText->setHtml(html);

    // 문법 강조 위젯 스크롤바 설정
    _syntaxText->verticalScrollBar()->setValue(
                _plainText->verticalScrollBar()->value());

    // 스크롤 동기화
    connect(_plainText->verticalScrollBar(), SIGNAL(valueChanged(int)),
            _syntaxText->verticalScrollBar(), SLOT(setValue(int)));

    connect(_syntaxText->verticalScrollBar(), SIGNAL(valueChanged(int)),
            _plainText->verticalScrollBar(), SLOT(setValue(int)));

    // 문법 강조 버튼 작동 불가능하게
    _highlightButton->setEnabled(false);
}


9 번째 줄: qDeleteAll() 은 주어진 컨테이너에 포함된 모든 포인터에 대해 C++ 의 delete 연산을 수행한다.

12 번째 줄: QString::prepend() 는 문자열을 앞에 추가한다.

16 번째 줄: QTextEdit::setHtml() 은 에디터의 내용을 주어진 HTML 로 설정한다.

19 번째 줄: QScrollBar::setValue() 는 스크롤바의 위치를 정한다. QScrollBar::value() 는 스크롤바의 현재 위치를 알려준다.

2.3.5 TokenParser 클래스

일반적으로 C++ 에서는 클래스 별로 소스 파일을 만들지만, 편의상 mainwindow.cpp 에 모두 포함하였다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
 * @brief 토큰 파서 클래스
 */

class TokenParser
{
public:
    /**
     * @brief TokenParser 생성자
     * @param s 파싱할 문자열
     */

    explicit TokenParser(const QString s = QString())
        : _s(s)
        , _currentPos(0)

    {
    }


TokenParser 생성자는 파싱할 문자열을 인자로 받아, 파싱할 문자열(_s) 파싱 위치(_currentPos)를 초기화한다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
    /**
     * @brief 남은 토큰이 있는지 확인
     * @return 토큰이 있으면 true, 없으면 false
     */

    bool hasNext() const
    {
        return _currentPos < _s.length();
    }

    /**
     * @brief 토큰을 읽고 다음으로 이동
     * @return 읽은 토큰
     */

    QString next()
    {
        return nextCommon();
    }

    /**
     * @brief 토큰을 읽지만 다음으로 이동하지 않음
     * @return 읽은 토큰
     */

    QString peekNext()
    {
        return nextCommon(false);
    }


hasNext() 는 파싱할 문자열이 남아 있는지 알려주고, next() 는 현재 토큰을 돌려주고 파싱 위치를 옮긴다. 반면에 peekNext() 는 현재 토큰을 돌려주되, 파싱 위치를 옮기지 않는다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    /**
     * @brief 현재 파싱 위치를 얻음
     * @return 현재 파싱 위치
     */

    int currentPos()
    {
        return _currentPos;
    }

    /**
     * @brief 파싱할 위치 설정
     * @param currentPos 새로운 파싱 위치
     */

    void setCurrentPos(int currentPos)
    {
        _currentPos = currentPos;
    }


currentPos() 는 현재 파싱 위치를 알려주고, setCurrentPos() 는 파싱 위치를 새로 설정한다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
private:
    QString _s;         /// 파싱할 문자열

    int _currentPos;    /// 파싱할 위치

    /**
     * @brief 토큰을 읽음
     * @param nextMode true 이면 다음으로 이동, 아니면 이동하지 않음
     * @return 읽은 토큰
     */

    QString nextCommon(bool nextMode = true)
    {
        int start = _currentPos;
        int end = _currentPos;
        QChar ch;

        // 연속된 문자, 숫자, _ 은 하나의 토큰
        while (end < _s.length()
               && ((ch = _s.at(end)).isLetterOrNumber() || ch == '_'))
            ++end;

        // 문자, 숫자, _ 가 아니면 한 문자가 하나의 토큰
        if (end < _s.length() && start == end
                && !((ch = _s.at(end)).isLetterOrNumber() || ch == '_'))
            ++end;

        // next mode 이면 파싱위치 이동
        if (nextMode)
            _currentPos = end;

        return _s.mid(start, end - start);
    }
};


nextCommon()next()peekNext() 의 공통 함수이다. nextMode 가 true 이면 next() 로, nextMode 가 false 이면 peekNext() 작동한다. 연속된 문자, 숫자, _ 의 조합은 하나의 토큰으로 인식하고, 그 이외의 문자들은 문자 하나를 토큰으로 처리한다.

2.3.6 TokenAbstract 클래스


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
 * @brief 토큰 처리를 위한 추상 클래스
 */

class TokenAbstract
{
public:
    enum TokenType {Nothing = 0, Keyword, Block};

    /**
     * @brief 보통 텍스트를 HTML 텍스트로 바꿈
     * @param plain 보통 텍스트
     * @return HTML 텍스트
     */

    static QString plainToHtml(const QString &plain)
    {
        QString result;

        for (int i = 0; i < plain.length(); ++i)
        {
            QChar ch(plain.at(i));

            if (ch == ' ')
                result.append("&nbsp;");
            else if (ch == '\n')
                result.append("<br/>");
            else if (ch == '<')
                result.append("&lt;");
            else if (ch == '>')
                result.append("&gt;");
            else if (ch == '&')
                result.append("&amp;");
            else
                result.append(ch);
        }

        return result;
    }


plainToHtml() 은 일반 텍스트를 HTML 로 바꾼다. 빠진 문자가 있을 지도...


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    /**
     * @brief TokenAbstract 생성자
     * @param token 토큰
     * @param color 색
     */

    TokenAbstract(const QString &token, const QString &color)
        : _token(token)
        , _color(color)
    {
    }

    /**
     * @brief TokenAbstract 소멸자
     */

    virtual ~TokenAbstract() {}


TokenAbstract() 생성자는 토큰과 색을 받아, 멤버 변수를 각각 초기화한다. ~TokenAbstract() 소멸자는 내용은 없지만, 상속을 고려해 빈 가상함수로 선언한다.


1
2
3
4
5
6
7
8
9
10
11
12
13
    /**
     * @brief 토큰 타입을 얻음
     * @return 토큰 타입
     */

    virtual TokenType type() const = 0;

    /**
     * @brief 토큰이 일치하는지 확인
     * @param token 토큰. 일치하는 토큰으로 바뀜
     * @param parser 토큰 파서
     * @return 일치하면 true, 아니면 false
     */

    virtual bool matched(QString *token, TokenParser *parser) const = 0;


type()matched() 는 각각 토큰의 종류와 주어진 토큰이 일치하는지 알려주는 순수가상함수이다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
    /**
     * @brief 현재 토큰을 얻음
     * @return 현재 토큰
     */

    virtual QString token() const
    {
        return _token;
    }

    /**
     * @brief 토큰의 색을 얻음
     * @return 토큰의 색
     */

    virtual QString color() const
    {
        return _color;
    }

    /**
     * @brief HTML 텍스트를 얻음
     * @return HTML 텍스트
     */

    virtual QString html() const = 0;

private:
    QString _token; /// 토큰
    QString _color; /// 색
};


token()color() 는 각각 현재 토큰과 색을 알려준다. html() 은 토큰을 HTML 로 돌려준다.

2.3.7 TokenKeyword 클래스

TokenAbstract 를 상속하여, 키워드를 토큰으로 처리한다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
 * @brief 키워드 토큰 클래스
 */

class TokenKeyword : public TokenAbstract
{
public:
    /**
     * @brief TokenKeyword 생성자
     * @param token 토큰
     * @param color 색
     */

    TokenKeyword(const QString &token, const QString &color)
        : TokenAbstract(token, color)
    {
    }

    bool matched(QString *token, TokenParser *parser) const Q_DECL_OVERRIDE
    {
        Q_UNUSED(parser);

        return *token == this->token();
    }

    QString html() const Q_DECL_OVERRIDE
    {
        return QString("<span style=\"color:%1\">").arg(color())
                .append(plainToHtml(this->token()))
                .append("</span>");
    }

    TokenType type() const Q_DECL_OVERRIDE
    {
        return Keyword;
    }
};


2.3.8 TokenDirective 클래스

TokenAbstract 를 상속하여 전처리기 지시자를 처리하는 클래스이다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
 * @brief 전처리기 지시자 클래스
 */

class TokenDirective : public TokenAbstract
{
public:
    /**
     * @brief TokenDirective 생성자
     * @param token 토큰
     * @param color 색
     * @param prefix 접두어
     */

    TokenDirective(const QString &token, const QString &color,
                    const QString &prefix = "#")
        : TokenAbstract(token, color)
        , _prefix(prefix)
        , _matched_token(prefix + token)
    {
    }


TokenDirective 생성자는 토큰과 색, 접두어를 인자로 받아, 멤버 변수드들 초기화한다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
    bool matched(QString *token, TokenParser *parser) const Q_DECL_OVERRIDE
    {
        // 파싱 위치 저장
        int savedPos = parser->currentPos();

        QString prefix(*token);

        // 접두어 확인
        while (prefix.length() < _prefix.length() &&
               _prefix.startsWith(prefix) && parser->hasNext())
            prefix.append(parser->next());

        if (prefix == _prefix)
        {
            QString nextToken;

            // 공백문자나 탭문자는 넘어감
            while (parser->hasNext() &&
                   ((nextToken = parser->peekNext()) == " " ||
                    nextToken == "\t"))
                prefix.append(parser->next());

            QString tkword(TokenAbstract::token());
            QString word;

            // 단어 확인
            while (word.length() < tkword.length() &&
                   tkword.startsWith(word) && parser->hasNext())
                word.append(parser->next());

            if (word == tkword)
            {
                *token = _matched_token = prefix + word;

                return true;
            }
        }

        // 파싱 위치 복원
        parser->setCurrentPos(savedPos);

        return false;
    }


matched() 함수는 "#" 과 "지시자" 사이에 공백이나 탭이 있는 경우도 일치하는 것으로 처리한다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    QString token() const Q_DECL_OVERRIDE
    {
        return _matched_token;
    }

    QString html() const Q_DECL_OVERRIDE
    {
        return QString("<span style=\"color:%1\">").arg(color())
                .append(plainToHtml(this->token()))
                .append("</span>");
    }

    TokenType type() const Q_DECL_OVERRIDE
    {
        return Keyword;
    }

private:
    QString _prefix;                /// 접두어
    mutable QString _matched_token; /// 일치한 토큰
};


2.3.9 TokenBlock 클래스

TokenAbstract 를 상속하여 주석같은 블럭 단위를 처리한다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
 * @brief 블럭 토큰 클래스
 */

class TokenBlock : public TokenAbstract
{
public:
    /**
     * @brief TokenBlock 생성자
     * @param token 토큰
     * @param endToken 끝나는 토큰
     * @param color 색
     */

    TokenBlock(const QString &token, const QString &endToken,
               const QString &color)
        : TokenAbstract(token, color)
        , _startToken(token)
        , _endToken(endToken)
        , _started(false)
    {
    }


TokenBlock() 생성자는 시작 토큰, 끝 토큰, 색을 인자로 받아 멤버 변수들을 초기화한다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
    bool matched(QString *token, TokenParser *parser) const Q_DECL_OVERRIDE
    {
        Q_UNUSED(parser);

        QString tkblock(_started ? _endToken : _startToken);

        QString tk(*token);

        // 파싱 위치 저장
        int savedPos = parser->currentPos();

        // 토큰 확인
        while (tk.length() < tkblock.length() &&
               tkblock.startsWith(tk) && parser->hasNext())
            tk.append(parser->next());

        if (tk == tkblock)
        {
            // 토큰 시작 상태 바꿈
            _started = !_started;

            *token = tk;

            return true;
        }

        // 파싱 위치 복원
        parser->setCurrentPos(savedPos);

        return false;
    }


matched() 함수는 시작 상태로 따라 시작 토큰 또는 끝 토큰으로 일치 여부를 확인한다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    QString token() const Q_DECL_OVERRIDE
    {
        return _started ? _startToken : _endToken;
    }

    QString html() const Q_DECL_OVERRIDE
    {
        QString tk(plainToHtml(this->token()));

        if (_started)
            tk.prepend(QString("<span style=\"color:%1;\">").arg(color()));
        else
            tk.append("</span>");

        return tk;
    }

    TokenType type() const Q_DECL_OVERRIDE
    {
        return Block;
    }


시작 상태에 따라 시작 토큰 또는 끝 토큰을 이용하며, 유형은 Block 이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    /**
     * @brief 블럭 내부인지 확인
     * @return 블럭 내부이면 true, 아니면 false
     */

    bool inner() const
    {
        return _started;
    }

    /**
     * @brief 블럭 내부 상태 해제
     */

    void reset()
    {
        _started = false;
    }

private:
    QString _startToken;    /// 시작 토큰
    QString _endToken;      /// 끝 토큰
    mutable bool _started;  /// 시작 상태
};

inner() 는 블럭 내부인지 알려주고, reset() 은 시작 상태를 초기화한다.

3. 마무리하면서...

C/C++ 에 대한 Syntax Highlighter 를 구현해 보았다. 키워드와 기호를 언어별로 지정해 준다면, 다양한 언어로 확장이 가능하다. 게다가 QTextEdit::toHtml() 을 이용하여 그 결과를 HTML 로 얻을 수도 있다.

그런데 사실 Qt에는 Syntax Highlighter 를 지원하는 QSyntaxHighlighter 라는 자체 클래스가 있다. 나름 자체적으로 Syntax Highlighter 를 구현해 보았지만, QSyntax Highlighter 를 쓰면보다 유연하고 확장성 있는 Syntax Highlighter 를 만들 수 있을 것이다. 

다음은 <Syntax Highlighter> 의 실행 화면이다.

전체 소스는 여기에서 확인하자.

댓글

이 블로그의 인기 게시물

토렌트: < 왕좌의 게임 > 시즌 1 ~ 시즌 8 완결편 마그넷

토렌트: NGC < 코스모스 > 우리말 더빙 전편(1편~13편) 마그넷

토렌트: < 스타워즈 > Ep.1 ~ Ep.6 마그넷