☰
Qt多文档编辑器实现:QMdiArea与QTextDocument完整指南
2026/10/7 17:50:14 网站建设 项目流程

简介:仿照微软Office Word界面与交互的Qt多文档编辑器完整工程,面向C++/Qt开发者,尤其适合作为MDI多文档界面、富文本处理、菜单栏与工具栏联动设计的课程设计或毕业设计参考。软件支持多文档同时编辑,窗口可平铺或层叠显示,保存格式为htm和html;功能上覆盖文件的新建、打开、保存、打印,编辑的撤销、重做、复制、剪切、粘贴,以及文本格式化中的字体粗体/下划线/斜体、字号、颜色和段落左对齐/居中/右对齐等操作。压缩包共80个文件,以图片资源为主,含46个bmp、8个png、4个gif、4个jpg,用于工具栏、菜单和界面美化;另有4个html样式文档、3个cpp和2个h源码、pro工程与qrc资源文件等,整体1.59MB。已有2461人学习下载,工程结构清晰,源码与资源分离,既能直接编译运行体验效果,也可作为二次开发基础,在此基础上扩展更多Word编辑功能。

1. Qt版Word多文档编辑:从一个“能开多个窗口”的编辑器说起

做 Qt C++ 桌面程序这些年,被问得最多的一个需求,不是界面多华丽,而是“能不能像 Word 一样,同时开好几个文档来回对照”。这个 Qt 版 Word 多文档编辑与处理完整版项目,解决的正是这件事:它用 QMdiArea 做多文档窗口骨架,用 QTextDocument 和 QTextEdit 做编辑内核,把打开、保存、查找替换、字体段落格式化、撤销重做都串成了一条可复用的链路。它适合三类人:一是正在做内部办公系统、需要批量处理 Word/TXT 文档的 Qt 开发者;二是想给现有工具加上“多标签/多窗口编辑”能力,但不想从零搭框架的人;三是学习 Qt 富文本编辑,想看一套完整工程怎么组织的人。我按这个完整版项目的思路重新拆了一遍,边拆边加实测参数和踩坑记录,下面的内容可以直接照着落地。

2. MDI 窗口骨架:QMdiArea 与 QTextDocument 的分工

2.1 为什么选 QMdiArea 而不是 QTabWidget

开始写多文档编辑器的时候,第一件要定的事是容器方案。很多人图快,直接拿 QTabWidget 塞 QTextEdit,一个标签一个文档,代码确实简单,但用起来很憋屈:两个文档没法并排看,想从 A 文件抄一段到 B 文件,只能在标签间来回切。真正贴近 Word 的操作习惯,还是得靠 QMdiArea。

QMdiArea 是 Qt 提供的多文档界面容器,它允许每个文档作为独立的 QMdiSubWindow 存在,子窗口可以拖拽、缩放、最小化、层叠或者平铺。这意味着用户可以同时看到两份文档,也可以用 Windows 菜单统一管理所有子窗口的排列方式。这套项目把主窗口的 centralWidget 直接设为 QMdiArea,文档子窗口由它统一接管,和 Word 的“窗口并排”体验是同一个逻辑。

维度QTabWidgetQMdiArea
多文档同屏不支持,只能激活单个标签支持并排、层叠、平铺
子窗口独立控制只有关闭标签有独立标题栏、最小化/最大化/关闭
活动窗口事件currentChanged(int)subWindowActivated(QMdiSubWindow*)
菜单合并需要自己维护当前页系统菜单可统一管理子窗口动作
实现复杂度低中,但换来的是完整桌面体验

初始化代码我一般是这么写的:

// mainwindow.cpp 构造函数里 m_mdiArea = new QMdiArea(this); m_mdiArea->setViewMode(QMdiArea::SubWindowView); m_mdiArea->setActivationOrder(QMdiArea::ActivationHistoryOrder); m_mdiArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); setCentralWidget(m_mdiArea);

这段代码做了三件事:第一行创建 MDI 容器;第二行设成子窗口视图模式,每个文档都能独立拖动;第三行设置窗口激活顺序按历史来,这样用户点哪个子窗口,最近操作过的窗口优先被激活。第四行是横向滚动条策略,文档宽了不会挤压其他窗口。实际工程里,如果暂时不需要像 Word 那样铺开对比,可以把 ViewMode 改成QMdiArea::TabbedView,那就是免维护的标签页模式,这个参数后面随时可以切。

2.2 QTextDocument 是数据层,QTextCursor 是操作手柄

多文档编辑器的核心不是窗口管理,而是里面的编辑内核。Qt 的富文本编辑是三件套:QTextEdit 负责视图交互,QTextDocument 负责数据存储,QTextCursor 负责对文档做修改操作。一开始容易搞混的是,QTextEdit 看起来既能显示又能编辑,为什么不直接把所有东西都塞给它?因为 QTextEdit 只是一个外壳,真正的文档结构和撤销重做栈全在 QTextDocument 里。

QTextDocument 里存的是块(block)、帧(frame)、表格(table)和字符格式(char format)这些结构化内容,它可以脱离界面直接操作。QTextCursor 则是作用在文档上的“光标手柄”,通过它插入文本、删除内容、修改格式。每次用 QTextCursor 修改文档,文档内部的撤销栈会自动记录,QTextEdit 只是把这个栈的开关暴露出来而已。

配套的初始化代码:

// 创建一个文档对象,并绑定到编辑器 QTextDocument *doc = new QTextDocument(this); doc->setUndoRedoEnabled(true); // 开启撤销重做栈,这是 Word 体验的前提 doc->setMaximumBlockCount(1000000); // 限制块数量,防止超大文档拖死 UI QTextEdit *editor = new QTextEdit(this); editor->setDocument(doc); // 编辑器显示 doc 内容 editor->setAcceptRichText(true); // 允许粘贴富文本,保留字体/颜色

这里有个容易忽略的参数:setAcceptRichText。很多初版项目只处理纯文本,把用户从网页或其他 Word 文档复制过来的内容直接丢掉了格式,体验很差。设成 true 之后,QTextEdit 会自动把 HTML 片段转成文档内部的富文本结构,粘贴进来的时候字体、加粗、颜色都还在。另一个值得注意的点是setMaximumBlockCount,它是一道保险,防止有人打开一个几百 MB 的文本文件直接把程序拖崩。

3. 打开与保存:编码识别、QSaveFile 与文件类型判断

3.1 编码识别:别再一刀切用 UTF-8

多文档编辑器处理 Word 和 TXT 文件时,第一个“翻车点”是编码。Windows 上很多老文档是 GBK/GB2312 编码,如果打开文件时直接用默认的 UTF-8,中文全变成乱码。这个项目里正确处理方式是先读原始字节,再判断编码。

// 从文件中读取原始字节 QFile file(filePath); if (!file.open(QIODevice::ReadOnly)) { QMessageBox::warning(this, "错误", file.errorString()); return; } QByteArray raw = file.readAll(); file.close(); // 根据 BOM 和内容特征选择编码 QTextCodec *codec = nullptr; if (raw.startsWith("\xEF\xBB\xBF")) { codec = QTextCodec::codecForName("UTF-8"); // BOM 标记 } else if (raw.startsWith("\xFF\xFE")) { codec = QTextCodec::codecForName("UTF-16LE"); // 小端 UTF-16 } else if (raw.startsWith("\xFE\xFF")) { codec = QTextCodec::codecForName("UTF-16BE"); // 大端 UTF-16 } else { // 没有 BOM,按 GBK 优先尝试,这是中文 Windows 文本最常见的编码 codec = QTextCodec::codecForName("GBK"); } QString content = codec ? codec->toUnicode(raw) : QString::fromUtf8(raw);

逻辑说明:先读文件字节流,再按 BOM 特征判断编码,最后用QTextCodec::toUnicode转成 Qt 内部的 Unicode 字符串。如果文件没有 BOM,GBK 是中文环境下的最高概率编码,优先尝试。有 BOM 则直接用对应编码,避免误判。

参数说明:toUnicode接收的是QByteArray原始字节,返回QString。这里要特别注意,QTextCodec 在 Qt 6 里被移到了 core5compat 模块,如果用的是 Qt 6,需要加上QT += core5compat并包含头文件QTextCodec。Qt 5.12 到 5.15 直接可用。

3.2 保存用 QSaveFile 而不是 QFile

保存这块,初学者容易直接用QFile打开文件然后write()。这个做法在文档长、写盘慢的时候有风险:程序崩了、断电了、磁盘满了,原文件可能只剩一半。完整版项目里用的是 QSaveFile,它是 Qt 提供的原子写文件方案。

// QSaveFile 先写临时文件,commit 时才真正替换原文件 QSaveFile outFile(filePath); if (!outFile.open(QIODevice::WriteOnly)) { QMessageBox::warning(this, "错误", outFile.errorString()); return; } // 自动判断当前文本类型:纯文本存 UTF-8,富文本存 HTML QTextDocument *doc = editor->document(); if (isPlainText) { outFile.write(doc->toPlainText().toUtf8()); } else { outFile.write(doc->toHtml().toUtf8()); } // 关键步骤:commit 失败不会破坏原文件 if (!outFile.commit()) { QMessageBox::warning(this, "保存失败", outFile.errorString()); }

逻辑说明:QSaveFile 打开后写的是同目录下的隐藏临时文件,写完调用commit()时,Qt 内部先fsync落盘,再用重命名方式替换原文件,整个过程是原子的。如果程序在中途崩溃,原文件还是完整的,最多留下一个临时文件。这个特性对多文档编辑器来说非常重要——用户开好几个文档,一关窗口全保存,任何一个保存失败都不该把之前的成果覆盖掉。

参数说明:commit()是必须要调的,如果只调write()不调commit(),数据不会真正落到目标文件。还有一个setDirectWriteFallback(true)参数,意思是某些文件系统不支持原子重命名时,退化为直接写,我一般保持默认 true,兼容网络盘和旧文件系统。

3.3 判断 txt/docx/doc:扩展名与特征字节看哪个

多文档编辑器的打开对话框通常允许过滤多种格式,但真实场景里用户会拿一个扩展名是.doc但实际内容是纯文本的文件。这类问题没法只信扩展名,得看文件头几个字节。

文件类型特征字节处理方式
TXT 纯文本无明显特征,或 BOM 开头直接用 QTextCodec 解码
DOCX(Word 2007+)PK(0x50 0x4B)本工程只做预览/提取文本,用 QuaZip 库解压
DOC(Word 97-2003)D0 CF 11 E0 A1 B1 1A E1OLE 复合文档,需要 libgsf 或 unoconv 转换
RTF 富文本{\rtfQTextDocument 原生支持,直接加载

这个项目对 docx 的处理方式,我是比较务实的:多文档编辑器不是要重写一个 Word,所以对 docx 采取“读文本/存文本”策略——用 QuaZip 打开 docx(它本质是 zip 包),读取word/document.xml里的正文,用简单的正则把<w:t>标签里的文字拼出来;保存时存成 txt/html,不强行写回 docx。这样做的好处是绕开了 OOXML 那一大堆命名空间细节,坏处是丢掉了批注、修订、复杂的样式。如果只是做办公辅助工具,完全够用。完整版项目里这部分代码封装成了一个DocxHelper类,接口就两个:extractText()和createSimpleDocx(),需要改格式模板时直接替换 XML 模板文件就行。

4. 编辑功能里的硬骨头:查找替换、字符格式与撤销栈

4.1 查找替换:循环定位别把自己绕死

多文档编辑器里查找替换是最常用的功能,但它有一个很隐蔽的坑:如果替换文本里包含查找文本,比如把“abc”替换成“abcd”,用同一个 cursor 继续往下找,就永远找不到下一个位置,程序会陷入死循环。正确做法是每次替换后,用一个新 QTextCursor 从替换结果的末尾继续找。

// 从指定位置开始查找 QTextCursor cursor = doc->find(searchText, fromPos, QTextDocument::FindCaseSensitively); int count = 0; while (!cursor.isNull()) { QTextCursor replCursor(cursor); // 复制查找结果的位置 replCursor.beginEditBlock(); // 让这次替换成为一步撤销 replCursor.insertText(replaceText); replCursor.endEditBlock(); count++; // 关键:从替换文本的末尾继续搜索,避免死循环 cursor = doc->find(searchText, replCursor.position(), QTextDocument::FindCaseSensitively); } statusBar()->showMessage(QString("共替换 %1 处").arg(count));

逻辑说明:doc->find(searchText, fromPos, flags)返回一个指向匹配文本开头的光标,如果没找到,返回的光标isNull()为 true。每次替换后,用replCursor.position()作为下一次搜索的起始位置,因为 insertText 之后光标已经停在替换文本末尾,所以下一次 find 不会重复命中本轮内容。

参数说明:QTextDocument::FindCaseSensitively表示大小写敏感,FindWholeWords表示全字匹配,两者可以用|叠加。如果做“全部替换”,我建议先把光标移到文档开头,再循环替换;如果做“单次替换”,则用当前选中的位置作为起点。这里面的beginEditBlock/endEditBlock也非常关键,它让多次 insertText 操作合并成一个撤销步骤,否则用户按一次 Ctrl+Z 只能撤回一个字符,体验极差。

4.2 字体和段落格式:beginEditBlock 保证撤销一致

对选中的文本设置字体、字号、加粗、颜色,是多文档编辑器的另一个高频操作。QTextCursor 提供了QTextCharFormat和QTextBlockFormat两类格式对象,前者管字符级样式,后者管段落级样式。

void MainWindow::onBoldClicked() { QTextCursor cursor = currentEditor()->textCursor(); // 如果当前选中了文本才处理 if (!cursor.hasSelection()) { return; } QTextCharFormat fmt; fmt.setFontWeight(cursor.charFormat().fontWeight() == QFont::Bold ? QFont::Normal : QFont::Bold); fmt.setFontPointSize(12); cursor.beginEditBlock(); // 合并格式操作为一步撤销 cursor.mergeCharFormat(fmt); cursor.endEditBlock(); }

逻辑说明:这段代码先判断当前光标是否选中了文本,没有选中直接返回。然后创建一个QTextCharFormat,用setFontWeight在加粗和正常之间切换,setFontPointSize(12)固定字号。mergeCharFormat会把现有格式和 fmt 合并,只改其中差异部分,不会把用户原本的字体颜色冲掉。

参数说明:mergeCharFormat和setCharFormat的区别是前者只修改 fmt 里设置了属性的项目,后者会整体替换。做加粗、斜体、下划线这种开关型操作时,一定要用mergeCharFormat。字体颜色用fmt.setForeground(QBrush(QColor(255,0,0))),背景色用fmt.setBackground(...)。段落对齐则在另一个格式类里:QTextBlockFormat的setAlignment(Qt::AlignCenter),然后cursor.mergeBlockFormat(blockFmt)。

4.3 撤销重做:新文档打开前必须清空栈

QTextDocument 的撤销栈是浮动的:每个文档对象都维护自己的 undo/redo 栈。多文档编辑器里最常见的撤销 bug,是用户打开新文档直接往里输入,然后按 Ctrl+Z,结果把上一个文档的内容撤掉了,或者内容错乱。原因是clearUndoRedoStacks()没被调用,旧文档的撤销信息还挂在同一个 QTextDocument 上。

void MainWindow::createNewDocument() { QTextDocument *doc = new QTextDocument(this); doc->setUndoRedoEnabled(true); doc->setModified(false); // 初始状态未修改 QTextEdit *editor = new QTextEdit(this); editor->setDocument(doc); // 打开新文档前,强制清空撤销栈 doc->clearUndoRedoStacks(); doc->setModified(false); addSubWindow(editor); }

逻辑说明:clearUndoRedoStacks()必须在文档开始接收新内容之前调用,否则旧编辑记录会污染新文档的撤销序列。setModified(false)用于标记文件未修改,窗口标题栏上就不会出现“改动未保存”的提示,等用户改动了内容,标题栏自动加*。这个标记在关闭子窗口判断“是否要保存”时会用到。

参数说明:注意撤销栈是按文档实例隔离的,不是按编辑器隔离的。如果两个 QTextEdit 共用一个 QTextDocument,它们共享同一个撤销栈,操作会互相影响。多文档场景下一定是一个编辑器配一个文档,不要复用。

5. 多文档编辑器常见问题与排查:五个高频坑

5.1 关闭子窗口没提示直接丢数据

现象:用户在一个文档里编辑了半小时,点子窗口右上角的 ×,内容直接没了,没有任何保存提示。

原因:QMdiSubWindow 的默认关闭行为是直接 close,它不知道 QTextEdit 里有没有未保存的修改。如果你只在主窗口菜单里处理了保存动作,没拦子窗口的关闭事件,用户点 × 就绕过了保存逻辑。

解决:在创建好 QMdiSubWindow 后,给每个子窗口安装事件过滤器,接管 Close 事件:

bool MainWindow::eventFilter(QObject *obj, QEvent *event) { if (event->type() == QEvent::Close) { QMdiSubWindow *sub = qobject_cast<QMdiSubWindow*>(obj); if (sub && editorForSubWindow(sub)->document()->isModified()) { QMessageBox::StandardButton ret = QMessageBox::question( this, "保存", "文档已修改,是否保存?", QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel); if (ret == QMessageBox::Save) { saveDocument(sub); // 先保存 } else if (ret == QMessageBox::Cancel) { event->ignore(); // 取消关闭 return true; } } } return QMainWindow::eventFilter(obj, event); }

这里的关键是event->ignore(),调用它之后子窗口会取消关闭动作,窗口保留在界面上。

5.2 切换窗口后加粗按钮作用到别的文档

现象:在文档 A 里选中文字点加粗,切换到文档 B 再点加粗,B 没变粗,反而 A 里的文字变粗了。

原因:工具栏按钮的槽函数写的是currentEditor()->textCursor(),但是 currentEditor 的获取逻辑写错了——只取了第一个打开的 QTextEdit,而不是当前活动子窗口里的那个。MDI 切换子窗口时,活动窗口变化事件没有被连接。

解决:用subWindowActivated信号维护一个当前编辑器指针:

// 构造函数里 connect(m_mdiArea, &QMdiArea::subWindowActivated, this, &MainWindow::onSubWindowActivated); void MainWindow::onSubWindowActivated(QMdiSubWindow *sub) { if (sub) { m_currentEditor = qobject_cast<QTextEdit*>(sub->widget()); // 同步工具栏的加粗/字号状态,避免界面和文档状态不一致 updateFormatActions(m_currentEditor); } }

setActivationOrder(ActivationHistoryOrder)在这里也起作用,它保证用户用 Ctrl+Tab 切换子窗口时,事件触发顺序符合直觉。

5.3 打开大文档直接卡死

现象:用户双击打开一个 50MB 的日志文件,程序卡了十几秒没响应,之后界面像幻灯片一样。

原因:QTextDocument 默认会一次性把整个文件解析成富文本结构。50MB 的纯文本,内部会生成上百万个 block 和 text fragment,解析和渲染都会爆掉 UI 线程。

解决:给文档设置setMaximumBlockCount(),并配合一个分页读取策略。这个项目里我通常的做法是先按行数读前 N 行展示,再异步加载剩余部分:

// 打开超大文件时,先读取前 5000 行,避免 UI 卡死 QFile file(filePath); file.open(QIODevice::ReadOnly); QTextStream in(&file); in.setEncoding(QStringConverter::System); QStringList firstLines; for (int i = 0; i < 5000 && !in.atEnd(); ++i) { firstLines << in.readLine(); } QTextDocument *doc = editor->document(); doc->setMaximumBlockCount(50000); // 内存里最多 5 万块,超出自动丢弃最旧块 editor->setPlainText(firstLines.join('\n')); doc->setModified(true);

这种“截断显示”方案牺牲了一部分编辑完整度,但换来了界面流畅。真实办公场景里,让人去编辑一个 50MB 的文档本来就不现实,把它当阅读器看反而更实用。

5.4 替换了一半内容不见了

现象:执行“全部替换”后,发现文档中间有一段内容莫名其妙消失了,而且撤销也只能撤回一部分。

原因:多半是查找替换的循环代码里,直接用初始的QTextCursor做替换插入,没有用副本。插入文本之后,原光标位置被文档结构变化弄乱了,后边的查找起点错位,导致中间一段被意外吞掉。

解决:回到第 4.1 节的写法,每次迭代都QTextCursor replCursor(cursor)复制一份,替换后从replCursor.position()重新开始。另外,替换之前一定要cursor.beginEditBlock(),替换完endEditBlock(),这样整个替换序列在撤销栈里只占一步,用户按一下 Ctrl+Z 就能全部撤销,不至于陷入半个文档被替换的恐慌。

5.5 中文编码探测偶尔抽风

现象:同一个文件,在 A 电脑上打开正常,在 B 电脑上打开乱码;或者同一个 GBK 文件,有时被识别成 UTF-8,有时被识别成 GBK。

原因:GBK 和 UTF-8 对于纯汉字内容是“两套各自兼容”的编码,没有 BOM 时只能靠字节统计做概率判断。汉字在 UTF-8 里是 3 字节,在 GBK 里是 2 字节,碰到少量英文和数字混淆时,概率计算会翻车。

解决:我在实际工程里的做法是三层判断,第一层看 BOM,第二层用 Qt 的QTextCodec::codecForLocale()优先,第三层才是特征统计。并且把识别结果回显给用户,状态栏常驻一个编码选择下拉框:

ui->encodingCombo->addItems({"UTF-8", "GBK", "GB18030", "BIG5", "UTF-16LE"}); connect(ui->encodingCombo, &QComboBox::currentTextChanged, this, [this](const QString &enc){ QTextCodec *codec = QTextCodec::codecForName(enc.toUtf8()); if (codec && m_currentEditor) { QByteArray raw = m_currentEditor->document()->toPlainText().toUtf8(); m_reloadContent = codec->toUnicode(raw); // 重新解码显示 } });

这个方法没法做到 100% 准确,但至少给了用户一个“后悔药”,发现乱码可以手动切换编码恢复内容,而不是只能关掉重开。

6. 进阶:自动备份与导出 PDF,把多文档工具做成生产力

6.1 自动备份:QTimer + QSaveFile 双保险

多文档编辑器最大的风险是一次性开八个文档,晚上下班前忘了逐个保存。我给这套工程加上了一个粗粒度的自动备份机制:每 5 分钟,把所有已修改并且超过 30 秒没有改动的文档,保存一份.bak到同目录隐藏位置。

// 构造函数里启动定时器 QTimer *backupTimer = new QTimer(this); backupTimer->setInterval(5 * 60 * 1000); // 5 分钟触发一次 connect(backupTimer, &QTimer::timeout, this, &MainWindow::backupAllDocuments); backupTimer->start(); void MainWindow::backupAllDocuments() { foreach (QMdiSubWindow *sub, m_mdiArea->subWindowList()) { QTextEdit *editor = qobject_cast<QTextEdit*>(sub->widget()); if (!editor || !editor->document()->isModified()) continue; // 只备份最近 30 秒内改过的文档,避免频繁写盘 if (editor->document()->lastModified().secsTo(QDateTime::currentDateTime()) < 30) { QString bakPath = sub->windowTitle() + ".bak"; QSaveFile bakFile(bakPath); bakFile.open(QIODevice::WriteOnly); bakFile.write(editor->document()->toPlainText().toUtf8()); bakFile.commit(); } } }

这里用lastModified()判断文档最近改动时间,防止定时器每次触发都把八个文档全量重写一遍。备份文件名直接叫标题.bak,出了状况找回来很容易。这套逻辑不复杂,但真能救命——尤其当客户下午三点改口要求“把上午那版找回来”的时候。

6.2 导出 PDF:QTextDocument::print 一行搞定

多文档编辑器的最后一个闭环是输出。完整版项目里常见的导出需求是“把编辑结果变成 PDF 发给别人”,Qt 的 QTextDocument 自带的打印支持可以直接复用:

void MainWindow::exportPdf() { QString filename = QFileDialog::getSaveFileName( this, "导出 PDF", QString(), "PDF 文件 (*.pdf)"); QPrinter printer(QPrinter::HighResolution); printer.setOutputFormat(QPrinter::PdfFormat); printer.setOutputFileName(filename); printer.setPageMargins(QMarginsF(15, 15, 15, 15), QPrinter::Millimeter); // 把当前文档内容按分页渲染到 PDF QTextDocument *doc = m_currentEditor->document(); doc->print(&printer); statusBar()->showMessage("PDF 导出完成", 3000); }

参数说明:QPrinter::HighResolution保证文字清晰;setPageMargins设了 15 毫米页边距,符合默认打印习惯;真正干活的是doc->print(&printer),它会自动按 QTextDocument 内部的页面大小分页。如果导出之前想调整纸张方向,用printer.setOrientation(QPrinter::Landscape)。注意,打印机制依赖系统打印机驱动,在无打印机环境(比如纯服务器)上,需要用QPdfWriter代替 QPrinter,这个类不依赖打印机设备,更适合部署。

从“窗口骨架”到“自动备份”再到“PDF 导出”,这套 Qt 版 Word 多文档编辑与处理项目给我的最大启示是:工程的价值不在某个单点的奇技淫巧,而在于把 QMdiArea、QTextDocument、QTextCursor、QSaveFile 这些组件编排成一条不怕出事的流水线。我曾经在一个客户项目里漏掉了subWindowActivated的状态同步,结果客户反馈“在两个文件之间切换后,加粗按钮会修改上一个文件”,那次线上事故之后,我把这套工程的每个信号槽连接都强制走了一遍检查清单:活动窗口变化、文档修改标记、关闭事件拦截、编码识别回退、撤销栈隔离,挨个核对。从那以后,凡是经我手的 Qt 文档工具,都会先补上这一层“保存后检查”,再交出去。希望这次的完整拆解,能让你在自己的多文档编辑器里少踩几个同样的坑。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询