Qt 로 만들자: Syntax Highlighter
Qt Creator 뿐만 아니라 요즘의 에디터들은 모두 Syntax Highlighting 기능을 가지고 있다. 이번에는 이 Syntax Highlighting 기능을 구현해 보자.
1. 요구사항
2. 코드 작성
2.1 프로젝트 작성
2.2 헤더 분석(mainwindow.h)
원본 텍스트를 위한 에디터와 문법강조된 텍스트를 위한 에디터가 각각 있고, 문법 강조를 실행하기 위한 버튼이 하나 있다.
23~24번째 줄: QTextEdit 는 Qt 에서 제공하는 텍스트 에디터 위젯이다. 일반 텍스트 뿐만 아니라 HTML4 의 일부를 지원한다.
2.3 소스 분석(mainwindow.cpp)
2.3.1 생성자와 소멸자
별 내용은 없다. 메뉴와 위젯을 초기화한다.
2.3.2 initMenus()
"파일" 메뉴를 추가하고, 그 메뉴에 "끝내기" 액션을 추가한다.
2.3.3 initWidgets()
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()
6 번째 줄: TokenParser 는 문자열로부터 토큰을 분리해 내기 위한 클래스이다. 이후에 알아본다. QTextEdit::toPlainText() 는 에디터의 내용을 일반 텍스트로 돌려준다.
9 번째 줄: TokenAbstract 는 토큰을 나타내기 위한 추상 클래스이다. 이후에 알아본다.
11~14 번째 줄: TokenBlock 은 블럭을 나타내는 토큰이다. 이후에 알아본다.
C/C++ 관련 토큰을 추가한다.
35 번째줄: TokenKeyword 는 문자+숫자+_ 로 이루어진 단어와 일반 기호를 나타내는 토큰이다. 이후에 알아본다.
49 번째줄: TokenDirective 는 전처리기 지시자를 나타내는 토큰이다.
5 번째 줄: TokenParser::hasNext() 는 토큰이 남아 있는지 알려준다.
7 번째 줄: TokenParser::next() 는 다음 토큰을 돌려준다.
17 번째 줄: TokenAbstract::matched() 는 토큰이 일치하는지 알려준다.
5 번째 줄: TokenAbstract::html() 은 토큰을 HTML 로 표현한다.
14 번째 줄: TokenAbstract::type() 은 토큰의 종류를 알려준다.
16 번째 줄: TokenBlock::reset() 은 블럭 시작 상태를 초기화한다.
19 번째 줄: TokenBlock::inner() 은 블럭 내부인지 알려준다.
27 번째 줄: TokenAbstract::plainToHtml() 은 주어진 토큰을 HTML 로 바꾸어 돌려준다.
9 번째 줄: qDeleteAll() 은 주어진 컨테이너에 포함된 모든 포인터에 대해 C++ 의 delete 연산을 수행한다.
12 번째 줄: QString::prepend() 는 문자열을 앞에 추가한다.
16 번째 줄: QTextEdit::setHtml() 은 에디터의 내용을 주어진 HTML 로 설정한다.
19 번째 줄: QScrollBar::setValue() 는 스크롤바의 위치를 정한다. QScrollBar::value() 는 스크롤바의 현재 위치를 알려준다.
2.3.5 TokenParser 클래스
일반적으로 C++ 에서는 클래스 별로 소스 파일을 만들지만, 편의상 mainwindow.cpp 에 모두 포함하였다.
TokenParser 생성자는 파싱할 문자열을 인자로 받아, 파싱할 문자열(_s) 과 파싱 위치(_currentPos)를 초기화한다.
hasNext() 는 파싱할 문자열이 남아 있는지 알려주고, next() 는 현재 토큰을 돌려주고 파싱 위치를 옮긴다. 반면에 peekNext() 는 현재 토큰을 돌려주되, 파싱 위치를 옮기지 않는다.
currentPos() 는 현재 파싱 위치를 알려주고, setCurrentPos() 는 파싱 위치를 새로 설정한다.
nextCommon() 은 next() 와 peekNext() 의 공통 함수이다. nextMode 가 true 이면 next() 로, nextMode 가 false 이면 peekNext() 작동한다. 연속된 문자, 숫자, _ 의 조합은 하나의 토큰으로 인식하고, 그 이외의 문자들은 문자 하나를 토큰으로 처리한다.
2.3.6 TokenAbstract 클래스
plainToHtml() 은 일반 텍스트를 HTML 로 바꾼다. 빠진 문자가 있을 지도...
TokenAbstract() 생성자는 토큰과 색을 받아, 멤버 변수를 각각 초기화한다. ~TokenAbstract() 소멸자는 내용은 없지만, 상속을 고려해 빈 가상함수로 선언한다.
type() 와 matched() 는 각각 토큰의 종류와 주어진 토큰이 일치하는지 알려주는 순수가상함수이다.
token() 과 color() 는 각각 현재 토큰과 색을 알려준다. html() 은 토큰을 HTML 로 돌려준다.
2.3.7 TokenKeyword 클래스
TokenAbstract 를 상속하여, 키워드를 토큰으로 처리한다.
2.3.8 TokenDirective 클래스
TokenAbstract 를 상속하여 전처리기 지시자를 처리하는 클래스이다.
TokenDirective 생성자는 토큰과 색, 접두어를 인자로 받아, 멤버 변수드들 초기화한다.
matched() 함수는 "#" 과 "지시자" 사이에 공백이나 탭이 있는 경우도 일치하는 것으로 처리한다.
2.3.9 TokenBlock 클래스
TokenAbstract 를 상속하여 주석같은 블럭 단위를 처리한다.
TokenBlock() 생성자는 시작 토큰, 끝 토큰, 색을 인자로 받아 멤버 변수들을 초기화한다.
matched() 함수는 시작 상태로 따라 시작 토큰 또는 끝 토큰으로 일치 여부를 확인한다.
시작 상태에 따라 시작 토큰 또는 끝 토큰을 이용하며, 유형은 Block 이다.
전체 소스는 여기에서 확인하자.
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(" "); else if (ch == '\n') result.append("<br/>"); else if (ch == '<') result.append("<"); else if (ch == '>') result.append(">"); else if (ch == '&') result.append("&"); 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 를 만들 수 있을 것이다.
그런데 사실 Qt에는 Syntax Highlighter 를 지원하는 QSyntaxHighlighter 라는 자체 클래스가 있다. 나름 자체적으로 Syntax Highlighter 를 구현해 보았지만, QSyntax Highlighter 를 쓰면보다 유연하고 확장성 있는 Syntax Highlighter 를 만들 수 있을 것이다.
다음은 <Syntax Highlighter> 의 실행 화면이다.
전체 소스는 여기에서 확인하자.
댓글
댓글 쓰기