别光写Demo!用Qt文本编辑器项目,搞懂这些C++/GUI核心设计模式

在C++和Qt开发中,很多初学者都能快速实现一个功能完整的文本编辑器Demo,但往往止步于"能跑就行"的阶段。当需求变更或功能扩展时,代码很快变得难以维护。本文将带你从工程化角度重构一个文本编辑器,重点探讨如何运用设计模式解决实际问题。

1. 单例模式:全局配置管理的最佳实践

文本编辑器通常需要管理字体、主题、最近打开文件等全局配置。如果这些配置散落在各处,不仅难以维护,还容易导致状态不一致。单例模式是解决这类问题的经典方案。

1.1 传统实现的痛点

观察原始代码,我们发现配置信息如 isUnSaved curFilePath 直接放在MainWindow类中。这种方式存在几个问题:

  • 其他组件无法直接访问这些状态
  • 状态变更逻辑分散在各个方法中
  • 难以实现配置的持久化存储
// 原始代码中的状态管理
private:
    bool isUnSaved;
    QString curFilePath;

1.2 单例模式重构

我们创建一个 EditorConfig 单例类来集中管理所有配置:

class EditorConfig : public QObject {
    Q_OBJECT
public:
    static EditorConfig& instance() {
        static EditorConfig theInstance;
        return theInstance;
    }

    bool isDocumentModified() const { return m_isModified; }
    QString currentFilePath() const { return m_currentFilePath; }
    
    void setDocumentModified(bool modified) { 
        m_isModified = modified;
        emit modificationChanged(modified);
    }
    
    void setCurrentFilePath(const QString& path) {
        m_currentFilePath = path;
        emit filePathChanged(path);
    }

signals:
    void modificationChanged(bool);
    void filePathChanged(const QString&);

private:
    EditorConfig() = default;
    ~EditorConfig() = default;
    
    bool m_isModified = false;
    QString m_currentFilePath = "未命名文件.txt";
};

关键改进点

  • 通过静态 instance() 方法确保全局唯一访问点
  • 使用信号机制通知状态变更
  • 禁止外部构造和析构,保证单例完整性

1.3 实际应用示例

在MainWindow中使用配置单例:

void MainWindow::updateTitle() {
    auto& config = EditorConfig::instance();
    QString title = config.currentFilePath();
    if(config.isDocumentModified()) {
        title += " *";
    }
    setWindowTitle(title);
}

// 连接配置变更信号
MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
    connect(&EditorConfig::instance(), &EditorConfig::modificationChanged,
            this, &MainWindow::updateTitle);
    connect(&EditorConfig::instance(), &EditorConfig::filePathChanged,
            this, &MainWindow::updateTitle);
}

2. 观察者模式:实现编辑状态的实时响应

文本编辑器的另一个常见需求是:当文档内容变化时,需要更新状态栏、修改标志、自动保存触发器等。观察者模式可以优雅地解决这种一对多的依赖关系。

2.1 Qt内置的信号槽机制

Qt本身就基于观察者模式设计,我们可以充分利用这一特性:

// 原始代码中的修改检测
bool MainWindow::maybeSave() {
    if (ui->textEdit->document()->isModified()) {
        // 弹出保存对话框
    }
}

2.2 扩展观察者模式

我们可以建立更完善的编辑状态通知系统:

class EditorStateNotifier : public QObject {
    Q_OBJECT
public:
    explicit EditorStateNotifier(QTextEdit* editor) : m_editor(editor) {
        connect(editor->document(), &QTextDocument::contentsChanged,
                this, &EditorStateNotifier::onContentChanged);
    }

    void addObserver(EditorStateObserver* observer) {
        m_observers.append(observer);
    }

private slots:
    void onContentChanged() {
        for(auto observer : m_observers) {
            observer->update(m_editor);
        }
        EditorConfig::instance().setDocumentModified(true);
    }

private:
    QTextEdit* m_editor;
    QList<EditorStateObserver*> m_observers;
};

观察者接口设计

class EditorStateObserver {
public:
    virtual void update(QTextEdit* editor) = 0;
    virtual ~EditorStateObserver() = default;
};

// 示例:字数统计观察者
class WordCountObserver : public EditorStateObserver {
public:
    void update(QTextEdit* editor) override {
        int words = editor->toPlainText().split(QRegExp("\\s+")).size();
        qDebug() << "当前字数:" << words;
    }
};

2.3 实际应用效果

// 在MainWindow中设置观察者
MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
    m_notifier = new EditorStateNotifier(ui->textEdit);
    m_notifier->addObserver(new WordCountObserver());
    m_notifier->addObserver(new SyntaxChecker());
}

3. 命令模式:实现可扩展的撤销/重做系统

文本编辑器最核心的功能之一就是撤销/重做。命令模式将操作封装为对象,使我们可以轻松实现这一功能。

3.1 命令接口设计

class EditorCommand {
public:
    virtual ~EditorCommand() = default;
    virtual void execute() = 0;
    virtual void undo() = 0;
    virtual QString text() const = 0;
};

3.2 具体命令实现

以文本插入命令为例:

class InsertTextCommand : public EditorCommand {
public:
    InsertTextCommand(QTextEdit* editor, const QString& text, int pos)
        : m_editor(editor), m_text(text), m_position(pos) {}
    
    void execute() override {
        QTextCursor cursor = m_editor->textCursor();
        cursor.setPosition(m_position);
        cursor.insertText(m_text);
    }
    
    void undo() override {
        QTextCursor cursor = m_editor->textCursor();
        cursor.setPosition(m_position);
        cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, m_text.length());
        cursor.removeSelectedText();
    }
    
    QString text() const override {
        return QString("插入文本 '%1'").arg(m_text.left(10));
    }

private:
    QTextEdit* m_editor;
    QString m_text;
    int m_position;
};

3.3 命令管理器

class CommandManager {
public:
    void execute(EditorCommand* cmd) {
        cmd->execute();
        m_undoStack.push(cmd);
        clearRedoStack();
    }
    
    void undo() {
        if(!m_undoStack.isEmpty()) {
            EditorCommand* cmd = m_undoStack.pop();
            cmd->undo();
            m_redoStack.push(cmd);
        }
    }
    
    void redo() {
        if(!m_redoStack.isEmpty()) {
            EditorCommand* cmd = m_redoStack.pop();
            cmd->execute();
            m_undoStack.push(cmd);
        }
    }
    
    void clear() {
        qDeleteAll(m_undoStack);
        qDeleteAll(m_redoStack);
        m_undoStack.clear();
        m_redoStack.clear();
    }

private:
    QStack<EditorCommand*> m_undoStack;
    QStack<EditorCommand*> m_redoStack;
    
    void clearRedoStack() {
        qDeleteAll(m_redoStack);
        m_redoStack.clear();
    }
};

3.4 与UI集成

void MainWindow::onTextInserted(int pos, const QString& text) {
    auto cmd = new InsertTextCommand(ui->textEdit, text, pos);
    m_commandManager->execute(cmd);
    updateUndoRedoActions();
}

void MainWindow::updateUndoRedoActions() {
    ui->actionUndo->setEnabled(m_commandManager->canUndo());
    ui->actionRedo->setEnabled(m_commandManager->canRedo());
    
    if(m_commandManager->canUndo()) {
        ui->actionUndo->setText("撤销 " + m_commandManager->undoText());
    } else {
        ui->actionUndo->setText("撤销");
    }
    
    if(m_commandManager->canRedo()) {
        ui->actionRedo->setText("重做 " + m_commandManager->redoText());
    } else {
        ui->actionRedo->setText("重做");
    }
}

4. MVC模式:分离数据与界面

虽然Qt的Widgets模块不是严格的MVC实现,但我们可以借鉴其思想来组织代码。

4.1 模型层重构

将文档数据与UI分离:

class DocumentModel : public QObject {
    Q_OBJECT
public:
    explicit DocumentModel(QObject* parent = nullptr);
    
    QString content() const;
    void setContent(const QString& content);
    
    bool loadFromFile(const QString& fileName);
    bool saveToFile(const QString& fileName) const;
    
signals:
    void contentChanged(const QString& newContent);

private:
    QString m_content;
};

4.2 视图-控制器整合

MainWindow作为视图和控制器的混合体:

class MainWindow : public QMainWindow {
    Q_OBJECT
public:
    explicit MainWindow(DocumentModel* model, QWidget* parent = nullptr);
    
private slots:
    void onContentChanged(const QString& content);
    void onSaveRequested();
    
private:
    DocumentModel* m_model;
    QTextEdit* m_textEdit;
    
    void setupConnections();
};

// 实现
MainWindow::MainWindow(DocumentModel* model, QWidget* parent) 
    : QMainWindow(parent), m_model(model) {
    m_textEdit = new QTextEdit(this);
    setCentralWidget(m_textEdit);
    
    connect(m_model, &DocumentModel::contentChanged,
            this, &MainWindow::onContentChanged);
    connect(m_textEdit, &QTextEdit::textChanged, [this]() {
        m_model->setContent(m_textEdit->toPlainText());
    });
}

void MainWindow::onContentChanged(const QString& content) {
    if(m_textEdit->toPlainText() != content) {
        m_textEdit->setPlainText(content);
    }
}

4.3 优势对比

特性 原始实现 MVC重构后
数据访问 直接操作UI组件 通过模型接口
可测试性 低,依赖UI 高,模型可单独测试
多视图支持 困难 容易,多个视图可共享模型
业务逻辑位置 分散在UI事件处理中 集中在模型层

5. 工厂模式:灵活创建文档类型

当需要支持多种文档类型(如Markdown、富文本等)时,工厂模式可以提供灵活的创建机制。

5.1 文档接口设计

class Document {
public:
    virtual ~Document() = default;
    virtual QString content() const = 0;
    virtual void setContent(const QString& content) = 0;
    virtual bool load(const QString& fileName) = 0;
    virtual bool save(const QString& fileName) const = 0;
    virtual QString typeName() const = 0;
};

5.2 具体文档实现

class PlainTextDocument : public Document {
public:
    QString content() const override { return m_content; }
    void setContent(const QString& content) override { m_content = content; }
    
    bool load(const QString& fileName) override {
        QFile file(fileName);
        if(!file.open(QIODevice::ReadOnly | QIODevice::Text))
            return false;
        
        m_content = QString::fromUtf8(file.readAll());
        return true;
    }
    
    bool save(const QString& fileName) const override {
        QFile file(fileName);
        if(!file.open(QIODevice::WriteOnly | QIODevice::Text))
            return false;
        
        return file.write(m_content.toUtf8()) > 0;
    }
    
    QString typeName() const override { return "Plain Text"; }

private:
    QString m_content;
};

5.3 文档工厂

class DocumentFactory {
public:
    static Document* createDocument(const QString& type) {
        if(type == "plaintext") return new PlainTextDocument();
        if(type == "markdown") return new MarkdownDocument();
        if(type == "richtext") return new RichTextDocument();
        return nullptr;
    }
    
    static QStringList supportedTypes() {
        return {"plaintext", "markdown", "richtext"};
    }
};

5.4 在编辑器中使用

void MainWindow::createNewDocument(const QString& type) {
    Document* doc = DocumentFactory::createDocument(type);
    if(!doc) {
        qWarning() << "未知文档类型:" << type;
        return;
    }
    
    if(m_currentDocument) {
        if(!maybeSave()) {
            delete doc;
            return;
        }
        delete m_currentDocument;
    }
    
    m_currentDocument = doc;
    ui->textEdit->setPlainText(doc->content());
    updateWindowTitle();
}

6. 策略模式:实现可插拔的文本处理算法

当需要支持多种文本处理方式(如不同语言的拼写检查、代码格式化等)时,策略模式可以提供灵活的算法切换。

6.1 文本处理策略接口

class TextProcessingStrategy {
public:
    virtual ~TextProcessingStrategy() = default;
    virtual QString process(const QString& input) = 0;
    virtual QString name() const = 0;
};

6.2 具体策略实现

class UpperCaseStrategy : public TextProcessingStrategy {
public:
    QString process(const QString& input) override {
        return input.toUpper();
    }
    
    QString name() const override {
        return "大写转换";
    }
};

class MarkdownToHtmlStrategy : public TextProcessingStrategy {
public:
    QString process(const QString& input) override {
        // 简化的Markdown转换
        QString html = input;
        html.replace("**", "<strong>", Qt::CaseInsensitive);
        html.replace("*", "<em>", Qt::CaseInsensitive);
        return html;
    }
    
    QString name() const override {
        return "Markdown转HTML";
    }
};

6.3 策略上下文

class TextProcessor {
public:
    explicit TextProcessor(TextProcessingStrategy* strategy = nullptr)
        : m_strategy(strategy) {}
    
    void setStrategy(TextProcessingStrategy* strategy) {
        m_strategy = strategy;
    }
    
    QString processText(const QString& input) {
        if(!m_strategy) return input;
        return m_strategy->process(input);
    }
    
    QString currentStrategyName() const {
        return m_strategy ? m_strategy->name() : "无";
    }

private:
    TextProcessingStrategy* m_strategy;
};

6.4 在编辑器中的应用

void MainWindow::onTextProcessingRequested() {
    QMenu menu;
    QActionGroup group(&menu);
    
    // 添加无策略选项
    QAction* noneAction = menu.addAction("无处理");
    noneAction->setCheckable(true);
    noneAction->setChecked(!m_textProcessor.currentStrategy());
    group.addAction(noneAction);
    
    // 添加所有可用策略
    for(auto strategy : m_availableStrategies) {
        QAction* action = menu.addAction(strategy->name());
        action->setCheckable(true);
        action->setChecked(m_textProcessor.currentStrategy() == strategy);
        group.addAction(action);
    }
    
    QAction* selected = menu.exec(QCursor::pos());
    if(!selected) return;
    
    if(selected == noneAction) {
        m_textProcessor.setStrategy(nullptr);
    } else {
        for(auto strategy : m_availableStrategies) {
            if(strategy->name() == selected->text()) {
                m_textProcessor.setStrategy(strategy);
                break;
            }
        }
    }
    
    // 应用当前策略重新处理文本
    QString processed = m_textProcessor.processText(ui->textEdit->toPlainText());
    ui->textEdit->setPlainText(processed);
}

7. 状态模式:管理编辑器复杂状态

文本编辑器通常有多种状态(如只读模式、编辑模式、全屏模式等),状态模式可以帮助我们优雅地管理这些状态转换。

7.1 状态接口设计

class EditorState {
public:
    virtual ~EditorState() = default;
    virtual void enter(MainWindow* editor) = 0;
    virtual void exit(MainWindow* editor) = 0;
    virtual void handleInput(MainWindow* editor, QKeyEvent* event) = 0;
    virtual QString name() const = 0;
};

7.2 具体状态实现

class EditModeState : public EditorState {
public:
    void enter(MainWindow* editor) override {
        editor->textEdit()->setReadOnly(false);
        editor->setStatusTip("编辑模式");
    }
    
    void exit(MainWindow* editor) override {
        editor->textEdit()->document()->setModified(true);
    }
    
    void handleInput(MainWindow* editor, QKeyEvent* event) override {
        // 普通编辑模式下的特殊处理
        if(event->key() == Qt::Key_Escape) {
            editor->switchState(PreviewModeState::instance());
        }
    }
    
    QString name() const override { return "Edit"; }
    
    static EditorState* instance() {
        static EditModeState theInstance;
        return &theInstance;
    }
};

class ReadOnlyModeState : public EditorState {
public:
    void enter(MainWindow* editor) override {
        editor->textEdit()->setReadOnly(true);
        editor->setStatusTip("只读模式");
    }
    
    void exit(MainWindow* /*editor*/) override {}
    
    void handleInput(MainWindow* editor, QKeyEvent* event) override {
        if(event->key() == Qt::Key_F2) {
            editor->switchState(EditModeState::instance());
        }
    }
    
    QString name() const override { return "ReadOnly"; }
    
    static EditorState* instance() {
        static ReadOnlyModeState theInstance;
        return &theInstance;
    }
};

7.3 状态上下文

在MainWindow中管理状态:

class MainWindow : public QMainWindow {
    // ...
public:
    void switchState(EditorState* newState) {
        if(m_currentState == newState) return;
        
        if(m_currentState) {
            m_currentState->exit(this);
        }
        
        m_currentState = newState;
        m_currentState->enter(this);
        
        updateStatusBar();
    }
    
    EditorState* currentState() const { return m_currentState; }
    QTextEdit* textEdit() const { return ui->textEdit; }

protected:
    void keyPressEvent(QKeyEvent* event) override {
        if(m_currentState) {
            m_currentState->handleInput(this, event);
        }
        QMainWindow::keyPressEvent(event);
    }

private:
    EditorState* m_currentState = nullptr;
    // ...
};

7.4 状态转换示例

void MainWindow::onToggleReadOnly() {
    if(currentState() == ReadOnlyModeState::instance()) {
        switchState(EditModeState::instance());
    } else {
        switchState(ReadOnlyModeState::instance());
    }
}

更多推荐