别光写Demo!用Qt文本编辑器项目,搞懂这些C++/GUI核心设计模式
别光写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());
}
}
更多推荐



所有评论(0)