30分钟打造专业级代码编辑器:Qt Widgets实现语法高亮与撤销重做
每次临时查看或修改代码片段时,系统自带的记事本总是让人抓狂——没有语法高亮、无法撤销操作、连基本的自动缩进都没有。作为开发者,我们值得拥有更好的工具。本文将带你用Qt Widgets快速构建一个功能完善的代码编辑器,重点实现两大核心功能:基于QSyntaxHighlighter的语法高亮和利用QUndoStack的撤销/重做机制。
1. 环境准备与项目创建
首先确保已安装Qt开发环境(推荐Qt 5.15或Qt 6.x版本)。打开Qt Creator,选择"文件"→"新建文件或项目",创建一个"Qt Widgets Application"项目,命名为CodeEditor。
在项目配置页面,保持默认选项即可。创建完成后,你会看到自动生成的mainwindow.h和mainwindow.cpp文件。我们需要在此基础上添加编辑器功能。
基础界面组件配置:
// mainwindow.h #include <QMainWindow> #include <QTextEdit> class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(QWidget *parent = nullptr); private: QTextEdit *textEdit; // 核心编辑组件 };// mainwindow.cpp MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { textEdit = new QTextEdit(this); setCentralWidget(textEdit); // 设置基础编辑器属性 textEdit->setLineWrapMode(QTextEdit::NoWrap); // 禁用自动换行 QFont font("Consolas", 10); // 使用等宽字体 textEdit->setFont(font); resize(800, 600); setWindowTitle("Code Editor"); }2. 实现语法高亮功能
语法高亮是代码编辑器的灵魂。Qt提供了QSyntaxHighlighter类来轻松实现这一功能。我们将创建一个专门的高亮器类来处理C++语法。
创建语法高亮器类:
// syntaxhighlighter.h #include <QSyntaxHighlighter> #include <QTextCharFormat> class SyntaxHighlighter : public QSyntaxHighlighter { Q_OBJECT public: SyntaxHighlighter(QTextDocument *parent = nullptr); protected: void highlightBlock(const QString &text) override; private: struct HighlightingRule { QRegExp pattern; QTextCharFormat format; }; QVector<HighlightingRule> highlightingRules; QTextCharFormat keywordFormat; QTextCharFormat singleLineCommentFormat; QTextCharFormat multiLineCommentFormat; QTextCharFormat quotationFormat; QTextCharFormat functionFormat; };实现高亮规则:
// syntaxhighlighter.cpp #include "syntaxhighlighter.h" SyntaxHighlighter::SyntaxHighlighter(QTextDocument *parent) : QSyntaxHighlighter(parent) { // 设置关键字格式(蓝色加粗) keywordFormat.setForeground(Qt::darkBlue); keywordFormat.setFontWeight(QFont::Bold); // 设置注释格式(灰色) singleLineCommentFormat.setForeground(Qt::gray); multiLineCommentFormat.setForeground(Qt::gray); // 设置字符串格式(深绿色) quotationFormat.setForeground(Qt::darkGreen); // 设置函数格式(深红色) functionFormat.setForeground(Qt::darkRed); functionFormat.setFontWeight(QFont::Bold); // 添加C++关键字规则 QStringList keywordPatterns; keywordPatterns << "\\bchar\\b" << "\\bclass\\b" << "\\bconst\\b" << "\\bdouble\\b" << "\\benum\\b" << "\\bexplicit\\b" << "\\bfriend\\b" << "\\binline\\b" << "\\bint\\b" << "\\blong\\b" << "\\bnamespace\\b" << "\\boperator\\b" << "\\bprivate\\b" << "\\bprotected\\b" << "\\bpublic\\b" << "\\bshort\\b" << "\\bsignals\\b" << "\\bsigned\\b" << "\\bslots\\b" << "\\bstatic\\b" << "\\bstruct\\b" << "\\btemplate\\b" << "\\btypedef\\b" << "\\btypename\\b" << "\\bunion\\b" << "\\bunsigned\\b" << "\\bvirtual\\b" << "\\bvoid\\b" << "\\bvolatile\\b"; foreach (const QString &pattern, keywordPatterns) { HighlightingRule rule; rule.pattern = QRegExp(pattern); rule.format = keywordFormat; highlightingRules.append(rule); } // 添加单行注释规则 HighlightingRule singleLineCommentRule; singleLineCommentRule.pattern = QRegExp("//[^\n]*"); singleLineCommentRule.format = singleLineCommentFormat; highlightingRules.append(singleLineCommentRule); // 添加多行注释规则 HighlightingRule multiLineCommentRule; multiLineCommentRule.pattern = QRegExp("/\\*.*\\*/"); multiLineCommentRule.format = multiLineCommentFormat; highlightingRules.append(multiLineCommentRule); // 添加字符串规则 HighlightingRule quotationRule; quotationRule.pattern = QRegExp("\".*\""); quotationRule.format = quotationFormat; highlightingRules.append(quotationRule); // 添加函数定义规则 HighlightingRule functionRule; functionRule.pattern = QRegExp("\\b[A-Za-z0-9_]+(?=\\()"); functionRule.format = functionFormat; highlightingRules.append(functionRule); } void SyntaxHighlighter::highlightBlock(const QString &text) { foreach (const HighlightingRule &rule, highlightingRules) { QRegExp expression(rule.pattern); int index = expression.indexIn(text); while (index >= 0) { int length = expression.matchedLength(); setFormat(index, length, rule.format); index = expression.indexIn(text, index + length); } } }在MainWindow中使用高亮器:
// 在MainWindow构造函数中添加 new SyntaxHighlighter(textEdit->document());3. 实现撤销/重做功能
Qt内置的QUndoStack提供了强大的撤销/重做框架。我们将利用它来实现编辑器的历史记录功能。
设置撤销/重做系统:
// mainwindow.h #include <QUndoStack> #include <QUndoView> class MainWindow : public QMainWindow { // ... private: QUndoStack *undoStack; QUndoView *undoView; // 可选:显示撤销历史的面板 };// mainwindow.cpp MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { // ...之前的初始化代码... // 初始化撤销系统 undoStack = new QUndoStack(this); // 可选:添加撤销历史面板 undoView = new QUndoView(undoStack); undoView->setWindowTitle(tr("Command List")); undoView->show(); // 连接文本编辑器的撤销/重做信号 connect(textEdit->document(), &QTextDocument::undoAvailable, this, [this](bool available) { // 更新UI中的撤销动作状态 ui->actionUndo->setEnabled(available); }); connect(textEdit->document(), &QTextDocument::redoAvailable, this, [this](bool available) { // 更新UI中的重做动作状态 ui->actionRedo->setEnabled(available); }); }创建自定义编辑命令:
// texteditcommand.h #include <QUndoCommand> #include <QTextCursor> #include <QTextDocument> class TextEditCommand : public QUndoCommand { public: enum Type { Insert, Remove }; TextEditCommand(Type type, QTextDocument *doc, const QTextCursor &cursor, const QString &text, QUndoCommand *parent = nullptr); void undo() override; void redo() override; private: Type cmdType; QTextDocument *document; int position; QString insertedText; QString removedText; };// texteditcommand.cpp #include "texteditcommand.h" TextEditCommand::TextEditCommand(Type type, QTextDocument *doc, const QTextCursor &cursor, const QString &text, QUndoCommand *parent) : QUndoCommand(parent), cmdType(type), document(doc) { position = cursor.position(); if (type == Insert) { insertedText = text; setText(QString("插入: %1").arg(text.left(10))); } else { removedText = cursor.selectedText(); setText(QString("删除: %1").arg(removedText.left(10))); } } void TextEditCommand::undo() { QTextCursor cursor(document); cursor.setPosition(position); if (cmdType == Insert) { cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, insertedText.length()); cursor.removeSelectedText(); } else { cursor.insertText(removedText); } } void TextEditCommand::redo() { QTextCursor cursor(document); cursor.setPosition(position); if (cmdType == Insert) { cursor.insertText(insertedText); } else { cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, removedText.length()); cursor.removeSelectedText(); } }连接编辑器操作到命令系统:
// 在MainWindow中添加槽函数 void MainWindow::onTextChanged() { // 这里可以添加更精细的命令管理逻辑 // 例如合并连续输入字符为单个命令 } void MainWindow::setupEditorConnections() { connect(textEdit, &QTextEdit::textChanged, this, &MainWindow::onTextChanged); // 连接撤销/重做动作 connect(ui->actionUndo, &QAction::triggered, undoStack, &QUndoStack::undo); connect(ui->actionRedo, &QAction::triggered, undoStack, &QUndoStack::redo); }4. 增强功能与界面优化
现在我们已经实现了核心功能,接下来添加一些实用功能来提升编辑器体验。
添加行号显示:
// linenumberarea.h #include <QWidget> #include <QTextEdit> class LineNumberArea : public QWidget { public: LineNumberArea(QTextEdit *editor) : QWidget(editor), textEdit(editor) {} QSize sizeHint() const override { return QSize(textEdit->lineNumberAreaWidth(), 0); } protected: void paintEvent(QPaintEvent *event) override { textEdit->lineNumberAreaPaintEvent(event); } private: QTextEdit *textEdit; };// 在QTextEdit子类中添加 int lineNumberAreaWidth() { int digits = 1; int max = qMax(1, blockCount()); while (max >= 10) { max /= 10; ++digits; } int space = 3 + fontMetrics().horizontalAdvance(QLatin1Char('9')) * digits; return space; } void lineNumberAreaPaintEvent(QPaintEvent *event) { QPainter painter(lineNumberArea); painter.fillRect(event->rect(), Qt::lightGray); QTextBlock block = firstVisibleBlock(); int blockNumber = block.blockNumber(); int top = (int)blockBoundingGeometry(block).translated(contentOffset()).top(); int bottom = top + (int)blockBoundingRect(block).height(); 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; } }添加查找/替换功能:
// mainwindow.h private slots: void find(); void replace(); void replaceAll(); private: QDialog *findDialog; QLineEdit *findLineEdit; QLineEdit *replaceLineEdit; QCheckBox *caseSensitiveCheckBox; QCheckBox *wholeWordsCheckBox;// mainwindow.cpp void MainWindow::setupFindDialog() { findDialog = new QDialog(this); findDialog->setWindowTitle(tr("查找/替换")); QVBoxLayout *layout = new QVBoxLayout; findLineEdit = new QLineEdit; replaceLineEdit = new QLineEdit; caseSensitiveCheckBox = new QCheckBox(tr("区分大小写")); wholeWordsCheckBox = new QCheckBox(tr("全字匹配")); QPushButton *findButton = new QPushButton(tr("查找")); QPushButton *replaceButton = new QPushButton(tr("替换")); QPushButton *replaceAllButton = new QPushButton(tr("全部替换")); QPushButton *closeButton = new QPushButton(tr("关闭")); connect(findButton, &QPushButton::clicked, this, &MainWindow::find); connect(replaceButton, &QPushButton::clicked, this, &MainWindow::replace); connect(replaceAllButton, &QPushButton::clicked, this, &MainWindow::replaceAll); connect(closeButton, &QPushButton::clicked, findDialog, &QDialog::close); QFormLayout *formLayout = new QFormLayout; formLayout->addRow(tr("查找:"), findLineEdit); formLayout->addRow(tr("替换为:"), replaceLineEdit); QHBoxLayout *optionsLayout = new QHBoxLayout; optionsLayout->addWidget(caseSensitiveCheckBox); optionsLayout->addWidget(wholeWordsCheckBox); QHBoxLayout *buttonLayout = new QHBoxLayout; buttonLayout->addWidget(findButton); buttonLayout->addWidget(replaceButton); buttonLayout->addWidget(replaceAllButton); buttonLayout->addWidget(closeButton); layout->addLayout(formLayout); layout->addLayout(optionsLayout); layout->addLayout(buttonLayout); findDialog->setLayout(layout); } void MainWindow::find() { QString searchString = findLineEdit->text(); if (searchString.isEmpty()) return; QTextDocument::FindFlags flags; if (caseSensitiveCheckBox->isChecked()) flags |= QTextDocument::FindCaseSensitively; if (wholeWordsCheckBox->isChecked()) flags |= QTextDocument::FindWholeWords; bool found = textEdit->find(searchString, flags); if (!found) { QMessageBox::information(this, tr("查找"), tr("找不到\"%1\"").arg(searchString)); } }添加主题支持:
// mainwindow.h private slots: void changeTheme(const QString &themeName); private: void setupThemes(); QMap<QString, QMap<QString, QColor>> themes;// mainwindow.cpp void MainWindow::setupThemes() { // 默认主题 QMap<QString, QColor> defaultTheme; defaultTheme["background"] = Qt::white; defaultTheme["text"] = Qt::black; defaultTheme["lineNumbers"] = Qt::lightGray; themes["Default"] = defaultTheme; // Dark主题 QMap<QString, QColor> darkTheme; darkTheme["background"] = QColor(53, 53, 53); darkTheme["text"] = Qt::white; darkTheme["lineNumbers"] = QColor(80, 80, 80); themes["Dark"] = darkTheme; // Solarized Light主题 QMap<QString, QColor> solarizedLight; solarizedLight["background"] = QColor(253, 246, 227); solarizedLight["text"] = QColor(101, 123, 131); solarizedLight["lineNumbers"] = QColor(238, 232, 213); themes["Solarized Light"] = solarizedLight; } void MainWindow::changeTheme(const QString &themeName) { if (!themes.contains(themeName)) return; QMap<QString, QColor> theme = themes[themeName]; QString styleSheet = QString( "QTextEdit {" " background-color: %1;" " color: %2;" "}" "LineNumberArea {" " background-color: %3;" "}" ).arg(theme["background"].name(), theme["text"].name(), theme["lineNumbers"].name()); setStyleSheet(styleSheet); // 同时更新语法高亮颜色 updateSyntaxHighlightingForTheme(themeName); }