news 2026/10/15 13:44:50

Qt读写XML文件:QTreeWidget加载与导出完整实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Qt读写XML文件:QTreeWidget加载与导出完整实现

简介:面向Qt初中级开发者,这组工程示例集中演示XML文件与QTreeWidget的双向操作:借助QDomDocument解析XML并将节点映射为树形条目,同时支持将QTreeWidget逐项导出为XML文档,还加入拖放调整层级的能力,适合需要实现XML可视化编辑、配置管理或数据浏览的桌面应用场景。压缩包共31个文件,以cpp/h源码、ui界面、qrc资源、vcxproj工程文件为主,另有png截图、xml数据与bak备份;整体6.89MB,结构完整,可直接打开工程编译运行。工程内包含主程序、QTreeWidget封装类、界面设计与XML示例数据,能够对照观察读取结果与导出格式;示例覆盖读取XML、加载到QTreeWidget、保存回XML、拖放调整节点等关键环节,并给出代码实现与逻辑说明,可帮助开发者快速理解QDomDocument与QTreeWidget的配合方式,并据此扩展属性编辑、增删节点、拖拽排序等自定义功能。该资源已有3712人学习,适合作为Qt XML模块入门与实战的参考模板。

1. Qt读写Xml文件这件事,为什么总得拉上QTreeWidget一起讲

一个很常见的桌面端需求:程序里用XML保存设备树、配置目录或分类结构,界面上要看层级、要能改,改完了还要原样保存回去。Qt里处理XML的类不少,但只有把“Qt读写Xml文件”和“QTreeWidget加载显示Xml文件内容、QTreeWidget项导出保存为Xml”放在一起看,才是完整闭环。QTreeWidget是Qt现成的树形控件,XML是天然层级数据格式,两者互为镜像:XML负责存储,树负责交互。这个组合适合谁?适合要快速交付一个XML树形编辑器、配置浏览器、报文查看工具的Qt项目,也适合不想引入完整Model/View框架直接拿控件落地的开发同学。它能解决的核心问题很具体:让XML可见、可编辑、可还原,而不是躺在文件里当黑匣子。

2. 三种Qt XML读取接口怎么选:先把解析模型和控件模型对齐

写代码之前,最该纠结的不是“用什么函数读”,而是选哪一种解析接口。选错了解析接口,后面所有代码都会围绕错误前提打转。把XML读进QTreeWidget,本质上是一次“树到树”的搬运,搬运路线的起点是XML解析器,终点是QTreeWidgetItem。不同解析器的读取方式、生命周期、内存特征完全不一样,直接决定了后面代码怎么写。

2.1 QDomDocument、QXmlStreamReader、QXmlQuery:三种读法差别在哪

常见做法是分三种接口考虑。

第一种QDomDocument(Qt XML模块),它把整个XML文档一次性加载进内存,生成一个可随机访问的DOM树。优点是修改方便:拿到某个节点,可以随便改属性、删子节点、调整顺序,最后再序列化回去。缺点也明显:内存里同时存在原始文件内容和DOM结构,一个大文件就能吃几百兆内存。适合配置文件这种“小、要频繁改”的场景,不太适合动不动几十MB的报文文件。

第二种QXmlStreamReader(Qt Core自带),它不建DOM树,而是把XML当做一个token流,每次readNext()取一个令牌:开始标签、结束标签、文本、注释、处理指令。它的实际内存占用极小,速度比DOM快很多,但访问是顺序的,想回头改已经读过的节点就要重新解析。如果目标是把XML搬进QTreeWidget,流式读取的思路和控件树构建过程出奇地合拍:读到一个开始标签,就创建一个item;读到子标签,就递归创建子item;读到一个结束标签,就回到上一层。

第三种QXmlQuery(Qt XML Patterns模块)是面向XQuery/XPath的,适合“在已有XML里做查询”,用来做树形展示属于杀鸡用牛刀,而且它和编辑器场景天然不太匹配。加载和展示XML树,默认选QXmlStreamReader就够。

表格对比更直观:

接口内存特征适合做什么和QTreeWidget配合的姿势
QDomDocument全量加载小文件多次修改遍历DOM建item,修改后序列化
QXmlStreamReader流式,几乎不占额外内存大文件顺序读取每遇开始标签建一个item
QXmlQuery查询引擎XQuery检索不直接,适合做二次加工

2.2 XML节点到QTreeWidgetItem的映射:标签名、属性、文本各放一列

要把XML内容完整搬到QTreeWidget,先想清楚一个节点怎么映射。这里的“完整”是指不能丢信息:标签名、属性、文本、子节点顺序,每一项都得有位置。我常用三列:第0列标签名,第1列属性,第2列文本。

第0列显示标签名最直观;第1列把属性拼成key="value"的字符串;第2列放元素文本。如果属性很多,一列装不下,也可以把属性摊成多个列,但对通用方案来说三列够用。真正关键的一点是:第1列只是给人看的显示层,属性应该另外存一份结构化的数据。QTreeWidgetItem提供了data(int column, int role)接口,可以往指定列塞任意QVariant。我的做法是把属性存成QVariantMap,挂在第0列的Qt::UserRole角色上,第1列文本只是从这份数据拼出来的展示。这样导出时从UserRole读出来直接用,不会因为显示片段截断或格式变化丢数据。

这也意味着,加载XML时就要把属性和文本分开落位,不能只拼一个字符串丢给用户看。

2.3 最小骨架:先用QXmlStreamReader写一个不带界面的解析器原型

很多人一上来就直接在QTreeWidget里写递归,结果逻辑和控件耦合在一起,出了问题不好定位。我习惯先写一个不依赖界面的解析原型,把XML结构打出来确认无误,再替换成创建item的代码。下面是最小骨架,只依赖Qt Core。

#include <QCoreApplication> #include <QFile> #include <QXmlStreamReader> #include <QDebug> void parseNode(QXmlStreamReader &xml, int depth) { QString indent = QString(" ").repeated(depth * 2); qDebug().noquote() << indent << "<" << xml.name().toString() << ">"; for (const QXmlStreamAttribute &attr : xml.attributes()) { qDebug().noquote() << QString(" ").repeated(depth * 2 + 2) << attr.name().toString() << "=" << attr.value().toString(); } while (!xml.atEnd()) { QXmlStreamReader::TokenType type = xml.readNext(); if (type == QXmlStreamReader::StartElement) { parseNode(xml, depth + 1); } else if (type == QXmlStreamReader::Characters) { if (!xml.isWhitespace()) { qDebug().noquote() << QString(" ").repeated(depth * 2 + 2) << "text:" << xml.text().toString(); } } else if (type == QXmlStreamReader::EndElement) { return; } } } int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); QFile file("sample.xml"); if (!file.open(QFile::ReadOnly | QFile::Text)) { qWarning() << "open file failed"; return 1; } QXmlStreamReader xml(&file); while (!xml.atEnd()) { QXmlStreamReader::TokenType type = xml.readNext(); if (type == QXmlStreamReader::StartElement) { parseNode(xml, 0); } } if (xml.hasError()) { qWarning() << "parse error:" << xml.errorString(); } return 0; }

逻辑说明:parseNode函数处理的是“当前token已经是一个开始标签”的情况。它先打印这个标签本身和属性,然后循环readNext()读取后续token;遇到子元素开始标签就递归,遇到非空文本就打印文本,遇到结束标签就return,表示这棵子树读完。main函数负责开文件、建QXmlStreamReader,并逐token往下读,遇到顶层开始标签就调用parseNode。

参数说明:depth控制缩进,方便在控制台里看清层级关系。xml.errorString()在解析错误时会给出带位置和原因的提示,是排查XML格式问题的第一入口。之所以用noquote(),是因为QDebug默认会给QString加引号,这里关掉引号能让输出更接近原始XML。

这个骨架跑通之后,把qDebug()部分换成QTreeWidgetItem的创建代码就是第3章的完整加载器。先把原型跑通,能省下不少后面和控件纠缠在一起的排查时间。

3. QTreeWidget加载Xml文件内容:递归建树代码与列布局配置

这章把原型桥接到控件上,核心是完成“XML节点到三列树项”的翻译,并处理大文件加载时的响应问题。

3.1 首次加载:一个函数把XML变成三列树

直接上代码。

#include <QTreeWidget> #include <QTreeWidgetItem> #include <QXmlStreamReader> #include <QFile> #include <QVariantMap> QTreeWidgetItem* createItemFromXml(QXmlStreamReader &xml, QTreeWidget *tree) { QTreeWidgetItem *item = new QTreeWidgetItem(tree); item->setText(0, xml.name().toString()); QVariantMap attrMap; QStringList attrTexts; for (const QXmlStreamAttribute &attr : xml.attributes()) { attrMap.insert(attr.name().toString(), attr.value().toString()); attrTexts << QString("%1=\"%2\"") .arg(attr.name().toString(), attr.value().toString()); } item->setData(0, Qt::UserRole, attrMap); item->setText(1, attrTexts.join(" ")); while (!xml.atEnd()) { QXmlStreamReader::TokenType type = xml.readNext(); if (type == QXmlStreamReader::StartElement) { QTreeWidgetItem *child = createItemFromXml(xml, tree); if (child) { item->addChild(child); } } else if (type == QXmlStreamReader::Characters) { if (!xml.isWhitespace() && !xml.text().toString().trimmed().isEmpty()) { item->setText(2, xml.text().toString()); } } else if (type == QXmlStreamReader::EndElement) { break; } } return item; } void loadXmlToTree(const QString &filePath, QTreeWidget *tree) { QFile file(filePath); if (!file.open(QFile::ReadOnly | QFile::Text)) { return; } QXmlStreamReader xml(&file); tree->clear(); tree->setColumnCount(3); tree->setHeaderLabels({QStringLiteral("标签"), QStringLiteral("属性"), QStringLiteral("文本")}); while (!xml.atEnd()) { QXmlStreamReader::TokenType type = xml.readNext(); if (type == QXmlStreamReader::StartElement) { QTreeWidgetItem *item = createItemFromXml(xml, tree); if (item) { tree->addTopLevelItem(item); } } } tree->expandToDepth(1); if (xml.hasError()) { qWarning() << "load XML error:" << xml.errorString(); } }

逻辑说明:createItemFromXml接收的QTreeWidget指针纯粹是拿来new item用的,创建出来的item先不挂到树上,等子节点都挂完,由调用方决定是addChild还是addTopLevelItem。这样函数才能递归复用。属性被存进第0列的Qt::UserRole,第1列文本只是展示。文本节点只在“既不是空白、且trim过不空”时才写入第2列,避免把换行和缩进也当成内容。

参数说明:expandToDepth(1)表示只展开第一层,让加载后界面不至于直接摊成几千行。如果文件不大,想全部展开,可以改成expandAll();但一次性展开会把大文件的数据结构完全暴露给界面,滚动起来也吃力,默认浅展开更稳妥。

3.2 属性不直接拼进第0列:两种存储方式决定导出质量

有人图省事,把属性拼成key=value key2=value2塞进第0列或第1列,加载时确实也能看,但导出时就麻烦了:你得从字符串里重新解析属性,属性值里的空格、等号、引号都会成为解析的雷区。更稳的做法是像我上面那样,用QVariantMap存结构化属性。

这里有一个细节:如果加载时没有把属性存进UserRole,后面一旦用户做了拖动排序、重命名、复制粘贴,属性文本还留在第1列,可能被界面显示机制截断或改动;反过来从第1列解析属性,解析规则稍微写错一点,就开始怀疑人生。把属性挂进data里,界面显示和内部数据互不干扰,是这类联动功能里性价比最高的一步。

// 从item里取属性,供导出函数使用 QVariantMap getAttrMap(QTreeWidgetItem *item) { QVariant v = item->data(0, Qt::UserRole); if (!v.isValid()) { return QVariantMap(); } return v.toMap(); }

参数说明:Qt::UserRole是Qt预留的自定义数据起点,和显示角色完全分离。一个item可以在多个列的不同角色下存储不同类型的数据,不只有文本一个维度。

3.3 大XML文件加载卡界面:batchSize、processEvents和展开策略

QTreeWidget在循环里被逐个addTopLevelItem时,每加一个item都会触发布局重算,节点多了界面就明显卡。我处理大XML文件时会在加载函数外围关闭更新,并分批让事件循环喘息,避免界面长时间假死。

void loadLargeXmlToTree(const QString &filePath, QTreeWidget *tree, int batchSize = 500) { QFile file(filePath); if (!file.open(QFile::ReadOnly | QFile::Text)) { return; } QXmlStreamReader xml(&file); tree->clear(); tree->setUpdatesEnabled(false); int count = 0; while (!xml.atEnd()) { QXmlStreamReader::TokenType type = xml.readNext(); if (type == QXmlStreamReader::StartElement) { QTreeWidgetItem *item = createItemFromXml(xml, tree); if (item) { tree->addTopLevelItem(item); if (++count % batchSize == 0) { tree->setUpdatesEnabled(true); QCoreApplication::processEvents(); tree->setUpdatesEnabled(false); } } } } tree->setUpdatesEnabled(true); tree->expandToDepth(1); if (xml.hasError()) { qWarning() << "load XML error:" << xml.errorString(); } }

逻辑说明:setUpdatesEnabled(false)不是阻止创建item,而是告诉Qt先别忙着重绘,等全部插入完再一次刷新。每插入500个顶层item时临时打开更新并调用processEvents(),让界面响应一下事件循环,避免在加载过程中被系统判定为“未响应”。

参数说明:batchSize取500是经验值,可以根据机器性能调成200到1000之间。processEvents()的代价是加载时间变长一点点,换来的是界面可拖动、可关闭,体验上好很多。要注意:processEvents()会把用户的新操作事件也派发进来,如果事件处理里又碰同一个tree,可能造成重入,所以加载期间最好用一个loading标志位挡一下其它入口。

4. QTreeWidget项导出保存为Xml:深度优先遍历与流式写出

现在树在界面上了,用户可能改了文本、拖动、删了节点。要导出一份新的XML,从QTreeWidget读数据开始。

4.1 深度优先遍历QTreeWidgetItem:从一个递归函数开始

QTreeWidgetItem自带层级结构,深度优先遍历是树导出的天然方式。写一个递归函数,把当前item写成XML节点,再逐个处理子item。

#include <QXmlStreamWriter> #include <QTreeWidgetItem> #include <QVariantMap> void writeItemToXml(QXmlStreamWriter &writer, QTreeWidgetItem *item) { if (!item) { return; } QString tagName = item->text(0).trimmed(); if (tagName.isEmpty()) { // 没有标签名的item不能导出,给一个默认名兜底 tagName = QStringLiteral("node"); } QVariantMap attrMap = item->data(0, Qt::UserRole).toMap(); QString text = item->text(2); bool hasText = !text.trimmed().isEmpty(); bool hasChildren = item->childCount() > 0; if (!hasChildren && !hasText) { writer.writeEmptyElement(tagName); for (auto it = attrMap.begin(); it != attrMap.end(); ++it) { writer.writeAttribute(it.key(), it.value().toString()); } return; } writer.writeStartElement(tagName); for (auto it = attrMap.begin(); it != attrMap.end(); ++it) { writer.writeAttribute(it.key(), it.value().toString()); } if (hasText) { writer.writeCharacters(text); } for (int i = 0; i < item->childCount(); ++i) { writeItemToXml(writer, item->child(i)); } writer.writeEndElement(); } void saveTreeToXml(const QString &filePath, QTreeWidget *tree) { QFile file(filePath); if (!file.open(QFile::WriteOnly | QFile::Text)) { return; } QXmlStreamWriter writer(&file); writer.setAutoFormatting(true); writer.setAutoFormattingIndent(2); writer.writeStartDocument(QStringLiteral("1.0"), true); for (int i = 0; i < tree->topLevelItemCount(); ++i) { writeItemToXml(writer, tree->topLevelItem(i)); } writer.writeEndDocument(); }

逻辑说明:writeItemToXml先读第0列作为标签名,再读Qt::UserRole里的属性map,接着判断有没有子节点和文本。如果既没有子节点也没有文本,就写空元素;否则写开始标签、属性、文本和子节点。遍历顺序是从当前item开始,深度优先递归到每一棵子树,最后回到顶层下一个item。

参数说明:writeStartDocument的第二个参数true表示声明standalone属性。setAutoFormatting(true)会让QXmlStreamWriter自动加缩进和换行,生成可读性好的XML。要注意,如果原XML是紧凑格式,经过这次导出的XML会和原文件长得不一样,但只要语义相同,通常可以接受。attrMap为空时writeAttribute循环自然跳过,不会产生多余输出。

4.2 空元素与文本内容的边界:writeEmptyElement什么时候该用

这一段单独拿出来说,是因为很多人在这里翻车。空元素在XML里是合法写法,<node/>和<node></node>语义上都表示空元素,但两种形态会在对比、diff、版本管理时造成无谓差异。QXmlStreamWriter专门提供了writeEmptyElement接口来保留“空”的信号。上面代码里,当节点既没有子项也没有文本时就走空元素分支。

提示:writeEmptyElement之后千万不能调用writeEndElement,否则会多闭合一次,把XML结构写坏。

相应地,文本内容的判定也要注意:QTreeWidget的列文本如果是空字符串,用trimmed()判断可以过滤掉用户误敲的空格。但如果一个元素确实需要保留文本中的空格,比如代码片段类内容,trimmed()会误伤。你的需求若是有意义的空格,判定条件应当改为!text.isNull() && !text.isEmpty(),把去空格这件事只留给展示层。

4.3 保存后立刻回读:一个往返验证函数把问题拦截在交付之前

导出的XML如果直接交给下游,一旦结构出错,问题定位很吃力。我的习惯是保存后立刻做一次“往返验证”:把刚写出的文件重新通过loadXmlToTree加载成另一棵树,然后递归比较两棵树的关键数据。这一步能抓出大部分编码、空元素和属性丢失问题。

bool verifyTreeEqual(QTreeWidgetItem *a, QTreeWidgetItem *b) { if (!a || !b) { return false; } if (a->text(0) != b->text(0)) { return false; } if (a->data(0, Qt::UserRole).toMap() != b->data(0, Qt::UserRole).toMap()) { return false; } if (a->childCount() != b->childCount()) { return false; } for (int i = 0; i < a->childCount(); ++i) { if (!verifyTreeEqual(a->child(i), b->child(i))) { return false; } } return true; } bool verifyRoundTrip(const QString &filePath, QTreeWidget *originalTree) { QTreeWidget reloadedTree; loadXmlToTree(filePath, &reloadedTree); if (originalTree->topLevelItemCount() != reloadedTree.topLevelItemCount()) { return false; } for (int i = 0; i < originalTree->topLevelItemCount(); ++i) { if (!verifyTreeEqual(originalTree->topLevelItem(i), reloadedTree.topLevelItem(i))) { return false; } } return true; }

逻辑说明:verifyRoundTrip把保存出来的文件重新加载到一棵临时QTreeWidget里,然后逐顶层项递归比较标签名、属性map、子项数量。只要有一处不一致,就说明保存链路有问题。这个函数放进自动测试也很顺手,改动树结构后跑一次,比人工检查靠谱得多。

参数说明:这里比较的是数据角色,不是第1列属性文本,因为属性文本只是展示层。如果连显示文本一起比较,加载时拼接格式稍有变化就会误报,容易淹没真正的结构问题。

5. Qt Xml与QTreeWidget联动避坑:乱码、属性丢失、自闭合标签排查

这章全是实战里容易踩的坑,每条都按“现象、原因、解决”来记录。遇到类似问题时可以直接对照排查。

5.1 中文乱码:现象、原因、解决

现象:XML文件在编辑器里看是正常中文,加载到QTreeWidget后显示成类似“打目”的乱码;或者界面显示正常,保存后再用其它工具打开却乱了。

原因:一个是读取侧文件编码和XML声明不一致;另一个是写入侧没有声明编码。QXmlStreamReader本身会按XML声明处理编码,但很多自产自用的XML文件压根没有声明,默认按UTF-8解析,而文件实际是GBK保存的,这时必然乱码。写入侧如果手动拼XML而不写声明,保存出去的文件被下游按错误编码解读也会乱。

解决:统一用UTF-8,并在文件里写清楚声明。用QXmlStreamWriter时,writeStartDocument("1.0", true)就足以让写出文件带XML声明。读取时如果遇到历史遗留的GBK文件,先用QTextStream配合QTextCodec将内容解码成Unicode字符串,再喂给QXmlStreamReader的addData接口。

5.2 属性丢了一部分:用列文本当数据源是坏习惯

现象:加载时第1列里明明能看到name="测试",保存后属性要么丢了,要么值被截断,要么整个键值对错位。

原因:导出时从第1列文本里用正则拆分属性,而属性值可能包含空格、引号、换行、URL参数里的等号,正则写不完整就漏拆。某些情况下,用户对列文本做了复制粘贴或批量替换,属性字符串被修改,导出就跟着错。

解决:回到第3章的做法——属性在加载时即存入Qt::UserRole的QVariantMap,导出时从data取,第1列只管显示。这个改动落地之后,基本没再因为属性丢失返工过。如果历史代码已经写成从文本解析,也请补一个保险:先用getAttrMap()取UserRole,取不到再解析第1列,并且解析逻辑要处理引号内的空格。

5.3<node/>变成<node></node>:空元素判断写错分支

现象:原XML里<item/>,经过树加载再导出,变成<item></item>。结构没错,但是做配置文件diff时每次都一大片差异,让人以为有人改过文件。

原因:导出函数无条件走writeStartElement和writeEndElement,没有区分空元素。QXmlStreamReader在读取<item/>时给出StartElement和EndElement两个相邻token,createItemFromXml创建的item自然没有子节点,第2列文本也是空,但导出端没判断这个状态。

解决:导出前判断childCount() == 0 && text.trimmed().isEmpty(),满足则调用writeEmptyElement,之后直接return,不写结束标签。第4.1节已经给了完整代码。给一个防呆片段:

if (item->childCount() == 0 && item->text(2).trimmed().isEmpty()) { writer.writeEmptyElement(tagName); // 不要在后面再调用 writeEndElement() return; }

5.4 保存后特殊字符双重转义:& 变 &amp;

现象:原XML中一段文本写的是AT&amp;T,加载到QTreeWidget里看是AT&T,保存后文件里却变成AT&amp;amp;T。看起来像见鬼,实际是转义层级没理清。

原因:QXmlStreamReader对字符数据做了解码,&amp;会变成&;QXmlStreamWriter的writeCharacters会再做一次转义,&又变回&amp;,这是正常且正确的。问题出在有人看到第2列显示AT&T后,想当然地先手动把&替换成&amp;,再交给writeCharacters,结果writeCharacters又把这里的&替换成&amp;,最终变成双份转义。

解决:不要手动做任何转义,直接调用writer.writeCharacters()。如果你需要处理的是属性值,同样调用writer.writeAttribute(name, value),这两个接口都会自动处理特殊字符。记住一条:读和写都交给QXmlStream家族处理,手动字符串处理等于给自己埋雷。

5.5 大文件导入时界面假死:忘记关闭更新和事件处理

现象:加载一个几十MB的XML,窗口标题变成“未响应”,转几分钟后恢复;任务管理器看内存也不高,就是界面卡。

原因:循环里addTopLevelItem触发了大量布局刷新,事件循环被阻塞,系统判定窗口无响应。这个在第3.3节已经给了方案,但有一类容易被忽略的情况是:顶部item非常多,展开操作也会卡。展开大量子项时,滚动条计算和绘制会消耗很多时间。

解决:加载时用setUpdatesEnabled(false)包住全量插入,中间按batchSize调processEvents;加载完成后先expandToDepth(1),不要一上来就expandAll。如果树必然有几十万节点,建议换思路:把XML转存成SQLite,需要展开到哪一层再查哪一层,QTreeWidget只做视图。这已经不是小优化,而是架构取舍。

6. 再进一步:可编辑树、增量保存与“不较劲”的取舍

到这里读、写、回读三条路都通了。最后说三个让这套方案更好用的习惯。

第一个习惯:限制可编辑的列。QTreeWidget默认item不可编辑,要给树加编辑能力时,很多人直接把整行所有列都设成可编辑,结果用户双击标签名,把<device>改成了<devices>,导出后整个XML结构变了。我一般只允许编辑第2列文本和第1列属性,第0列标签名保持只读,并且在itemChanged信号里加一个校验:新文本里不能有空格、不能以数字开头、不能包含<>&。标签名是XML合法的门面,别让用户一脚踹开。

第二个习惯:大文件不要反复全量导出。QXmlStreamWriter写一个几十MB的XML用时不算大,但如果文件几百MB,每次保存都重写全部内容就不划算了。常见的做法是保存前用第4.3节的verifyRoundTrip先验证一次,同时做增量保存:记录用户改过哪些item的指针,保存时只重写这些item最近的一个XML祖先。实现上,可以在itemChanged里把item指针丢进QSet,保存时遍历QSet,找出对应的祖先层级,用QXmlStreamWriter重建那一段。这个方案要求树加载时额外记录每个item的XML路径,结构上更复杂;如果文件没那么大,直接全量写更省心。

第三个习惯:在“够用”和“性能”之间不较劲。QDomDocument确实写起来直观,但XML文件超过二三十MB时内存翻倍让人难受;QXmlStreamReader/Writer性能好,但如果你中途要随机改数据,流式解析就憋屈。我的取舍是:小于20MB、且需要频繁随机改的文件用QDomDocument,配合QTreeWidget做双向同步最省事;大于20MB的用QXmlStreamReader/Writer做全量加载和全量保存;超过200MB就不该往QTreeWidget里塞了,换个数据源和视图模型是正事。

最后讲一个我自己改不掉的毛病:无论保存逻辑多简单,保存完一定跑一次verifyRoundTrip。之前有段时间图省事,导出的文件总觉得“没谁会较真”,结果下游同事每天拿着diff工具找我核对格式。后来每次导完立刻回读比对,量少的时候一秒钟就能发现问题,量大的时候也就多等一两秒。这个习惯救过我很多次。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 13:43:33

自建Homebrew Tap封装oclint-0.15实现静态分析工具链延续

1. 为什么非得自己搭一个oclint-0.15的brew仓库&#xff1f;——不是“想造轮子”&#xff0c;而是官方早已断供你刚接手一个老iOS项目&#xff0c;代码质量扫描工具链里写着oclint-json-compilation-database&#xff0c;CI流水线里还跑着oclint-json-compilation-database --…

作者头像 李华
网站建设 2026/10/15 13:43:24

用Docker封装GPU推理环境:镜像构建、部署与故障排查指南

去年我接了一个语音识别服务的上线任务。模型在开发机上跑得飞快&#xff0c;精确度、延迟、显存占用全都无可挑剔&#xff0c;结果一交到部署环境&#xff0c;先是缺libcuda.so&#xff0c;再是 cuDNN 版本对不上&#xff0c;最后发现 TensorRT 跟手头的 CUDA 又是两套体系。来…

作者头像 李华
网站建设 2026/10/15 13:42:43

Python办公文档自动化:Word、Excel、PPT创建与解析实战

1. 为什么办公文档自动化值得你花时间日常工作中&#xff0c;Excel报表、Word合同、PPT汇报材料这三样东西几乎占据了每个职场人的大半时间。手动整理一份月度数据汇总&#xff0c;从十几个Excel文件里复制粘贴、调整格式、生成图表&#xff0c;再导出成Word报告——这套流程走…

作者头像 李华
网站建设 2026/10/15 13:42:32

Superpowers不是插件:AI编程能力的本质是本地代码图谱

1. “Superpowers”不是功能开关&#xff0c;而是开发者工具链的智能增强范式 最近在多个技术社区和开发者的私聊里&#xff0c;频繁看到“superpowers”这个词被当作某种神秘插件、一键开挂按钮&#xff0c;甚至有人以为装上就能自动写完整个项目。我第一次在 Cursor 的设置页…

作者头像 李华
网站建设 2026/10/15 13:42:24

OpenCV图像处理入门:从数组本质到滤波边缘检测实战

简介&#xff1a;OpenCV图像处理基础入门资料&#xff0c;以PDF电子书形式整理&#xff0c;面向计算机视觉初学者与人工智能方向的开发人员。内容围绕图像读取、显示、合并、边界填充、图像融合、阈值处理等核心操作展开&#xff0c;附有可运行的详解代码&#xff0c;帮助读者快…

作者头像 李华
网站建设 2026/10/15 13:41:40

Ad hoc网络最大不相交路径:从节点到地理的三层抗毁设计

1. 这不是普通路由优化——它直击Ad hoc网络的“命门”痛点你有没有试过在野外临时搭建一个无线通信网&#xff1f;比如某高校实验室组织的应急通信模拟演练&#xff0c;十几台移动节点&#xff08;笔记本、树莓派、无人机载终端&#xff09;散落在操场、树林和教学楼之间&…

作者头像 李华