news 2026/9/29 3:39:08

从Qt Address Book示例吃透Model/View自定义表格模型

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从Qt Address Book示例吃透Model/View自定义表格模型

1. 为什么建议你把Address Book这个示例重新翻出来读一遍

如果你手上正装着QT5.14.2,那么安装目录里自带的那套Examples其实是笔被大多数人忽略的财富。我最近因为要做一个设备参数表的管理界面,又把widgets/itemviews/addressbook这个Address Book示例从头到尾翻了一遍,越看越觉得它值得单独拿出来讲。原因很简单:市面上讲Qt Model/View的文章,要么停留在概念层面,只告诉你QAbstractTableModel要重写哪几个虚函数,要么一上来就是几万行的商业项目代码,看得人云里雾里。而Address Book这个示例,恰好卡在一个非常舒服的中间地带——它代码量不大,但把一个可编辑、可过滤、可排序、能持久化的数据管理界面该有的东西全都趟了一遍。

它本质上是一个通讯录程序:能添加联系人、编辑联系人、删除联系人、按名字过滤显示、把数据保存成CSV文件再读回来。功能听起来平平无奇,但实现方式却是标准的Qt Model/View范式——自定义TableModel承载数据,QSortFilterProxyModel做中间代理,QTableView负责呈现,再配上一个自定义的AddDialog对话框录入信息。对于刚接触模型视图框架、想搞清楚"数据到底是怎么从内存里跑到屏幕上的"的人来说,这套代码的清晰程度是教科书级别的。

这篇文章不是官方文档的复述。官方文档告诉你"每个虚函数的作用是什么",而我想跟你聊的是"当年我第一次抄这套代码时踩了哪些坑""那几个begin/end函数为什么少写一个程序就崩""过滤和排序为什么有时候不起作用"。如果你平时用QTableWidget用得很顺手,但一遇到数据量大、逻辑复杂就浑身难受,那这个示例就是你升级到自定义模型的起点。读完之后,你应该能独立搭出一个属于自己的表格数据管理界面,而不是每次都在网上到处找人现成的代码。

说实话,Address Book这个示例的完整工程目录里,真正有技术含量的文件就那么两三个:tablemodel.h/cpp是灵魂,addressbook.h/cpp是骨架,adddialog.h/cpp是血肉,.ui文件是皮相。我会重点把tablemodel这一块掰开揉碎讲,因为这里面的每一个override函数,背后都对应着模型视图框架的一个约定。理解了这些约定,你后面写任何自定义模型都是照猫画虎。

2. 这个示例的整体架构到底聪明在哪

2.1 三层结构:数据、代理、视图各司其职

Address Book最值得学的地方,是它没有把数据、过滤和显示揉在一起,而是老老实实分成了三层。最底下是TableModel,它继承自QAbstractTableModel,内部维护一个QList<Contact>,Contact是一个只有name和address两个QString成员的小结构体。数据本身完全由这个模型持有,外面谁想拿数据,都得通过模型的接口,而不是直接去碰那个列表。这一点非常关键——很多新手写自定义模型,习惯把内部容器暴露成public,结果外面随手一改,模型自己都不知道数据变了,界面自然就不同步。

中间层是QSortFilterProxyModel,也就是代理模型。它自己不存任何数据,而是套在TableModel外面,把源模型的数据"映射"一份出来。你在界面上看到的那张表,其实显示的是代理模型的数据,而不是源模型的。当你在搜索框里输入几个字,AddressBook会把输入内容设置给代理模型的setFilterFixedString,代理模型内部就会把不匹配的行过滤掉,但源模型里的数据一行都没动。等清空搜索框,所有数据又原封不动地回来了。这种"过滤不伤原始数据"的设计,是代理模型最大的价值。

最上层是QTableView。它只负责画格子、接收键盘鼠标事件,至于要显示几行几列、每格显示什么内容、表头写什么,它全都要回过头去问模型。视图和模型之间靠信号槽通信:模型数据变了会发dataChanged,增删了行会发rowsInserted或rowsRemoved,视图收到信号就局部重绘。这种解耦带来的最大好处是——同一个模型可以同时喂给QTableView、QListView、甚至QTreeView,代码复用率极高。

提示:不要把代理模型理解成"数据的中转站",它更像一层"带条件的透明玻璃"。你透过玻璃看数据,玻璃可以按你的要求模糊掉一部分,但玻璃后面的东西本身没变化。

三层分好之后,AddressBook主窗口的角色就变得很纯粹:它只负责在界面控件(按钮、文本框、表格)和模型之间做信号槽的接线。比如点"Add"按钮,它弹对话框;对话框返回结果后,它调用源模型的insertRows和setData把新数据塞进去;"Filter"输入框内容变了,它调代理模型的setFilterFixedString。逻辑清晰到你可以对着代码一行行念下来。

2.2 为什么不用QTableWidget而自己写模型

这个问题几乎每个看过示例的人都会问一句。QTableWidget多简单啊,setItem(row, col, new QTableWidgetItem("xxx")),几行代码就能把表填满。而TableModel要重写七八个虚函数,看起来费劲多了。那为什么Qt要专门做个Address Book示例来展示自定义模型?

根本原因在于数据的所有权和扩展性。QTableWidget是"数据存在控件里",每个格子里的内容都是一个QTableWidgetItem对象,由控件自己管理。你的业务数据(比如一个Contact结构体)反而成了外围,你得自己想办法从那一堆item里再读回来。当数据源不是本地内存列表,而是数据库、网络接口或者别的对象时,这种模式就会变得非常别扭——你不得不在数据源和表格控件之间反复搬运、同步。

自定义模型则反过来,数据的所有权在你手里。TableModel内部那个QList<Contact>才是唯一真相来源,界面只是它的一个"投影"。你想加过滤、加排序,套个代理模型就行;你想让同一份数据同时显示成表格和下拉列表,把同一个模型挂两个视图上就行;你想做数据校验、权限控制,在setData里加判断就行。这些能力在QTableWidget上做起来都很别扭。

再从性能上讲,QTableWidget为每个单元格创建一个对象,一万行两列就是两万个item对象,内存吓人。而自定义模型按需提供数据,视图问哪个格子就返回哪个格子的内容,不需要预先创建任何对象。Address Book数据量不大,感受不出来,但你把这个模式套到几千上万行的场景里,差距立刻显现。

2.3 从CSV持久化看数据与界面解耦的好处

Address Book用CSV文件存数据,导出的时候遍历TableModel里的Contact列表,逐行写文件;导入的时候逐行读,解析出name和address,再插入模型。整个过程里,界面上的QTableView完全不参与,它甚至不知道有人在读文件——只要模型发个信号说"数据变了",它重绘一下就行。

这种解耦在实际项目里太有用了。比如你的数据可能是从数据库查出来的,那么你只需要在模型里持有查询结果,读写数据库的逻辑放在模型之外,界面代码一行都不用改。又比如你想支持"导出为vCard"(示例里exportAsVCard函数就演示了这个),也完全不需要动视图。视图只管展示,模型只管数据,业务逻辑各归各位,改一个地方不会牵一发而动全身。

3. 逐个拆解TableModel里的关键函数

3.1 数据供给四件套:rowCount、columnCount、data、headerData

模型视图框架和视图之间其实有个隐含协议:视图会不停向模型提问"你有几行""你有几列""第r行第c列显示什么""第c列表头写什么"。TableModel要做的就是老老实实回答这四个问题。rowCount返回contacts.count(),columnCount固定返回2(名字和地址两列)。这两个函数看起来无聊,但它们决定了表格的规模,返回错了表格就会显示异常——返回0整个表就是空的,返回大了会访问越界。

data是信息量最大的一个。视图问某一格的内容时,会带上index和role两个参数。index包含行号列号,role则是问"你要哪种数据"——Qt::DisplayRole是要显示的文字,Qt::EditRole是要编辑时用的值,Qt::TextAlignmentRole是对齐方式,Qt::ToolTipRole是悬停提示。示例里最经典的写法就是根据role分支返回不同内容:显示和编辑都返回字符串,对齐角色返回居中等。

QVariant TableModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) return QVariant(); if (role == Qt::DisplayRole || role == Qt::EditRole) { const Contact &contact = contacts.at(index.row()); if (index.column() == 0) return contact.name; else if (index.column() == 1) return contact.address; } return QVariant(); }

这里有个容易忽略的点:index.isValid()这个判断必须写。因为视图在某些场景下会传入无效索引来试探模型,不判断就可能越界访问列表直接崩溃。我当年第一次抄这段代码时嫌麻烦省了它,结果一编辑单元格程序就挂,排查了大半天才发现是这里。

headerData负责回答表头写什么。它需要判断orientation是水平还是垂直:水平表头(列名)返回"Name""Address",垂直表头(行号)一般返回section + 1。很多人第一次写会忘记区分方向,结果行号和列名全乱套。

3.2 可编辑模型的双刃剑:flags和setData必须成对出现

一个模型默认是只读的,你想让某个格子能双击编辑,就必须在flags函数里给这个索引加上Qt::ItemIsEditable标志位。同时还要保留Qt::ItemIsEnabled和Qt::ItemIsSelectable,否则格子会灰掉或者选不中。示例的做法是:如果索引有效就返回Qt::ItemIsSelectable | Qt::ItemIsEditable | Qt::ItemIsEnabled,无效就返回Qt::NoItemFlags。

光有flags还不够,编辑完成后视图会把新值通过setData回调给你,你得负责把它写回内部数据。setData里要判断role是不是Qt::EditRole,是的话取出对应的Contact引用,把value.toString()写进对应的name或address字段,然后发一个dataChanged信号通知视图"这一格变了,你重绘一下"。

bool TableModel::setData(const QModelIndex &index, const QVariant &value, int role) { if (!index.isValid() || role != Qt::EditRole) return false; Contact &contact = contacts[index.row()]; if (index.column() == 0) contact.name = value.toString(); else if (index.column() == 1) contact.address = value.toString(); else return false; emit dataChanged(index, index, {Qt::DisplayRole, Qt::EditRole}); return true; }

注意:dataChanged信号至少要传两个参数——起止索引。哪怕只改了一格,也要把起始和结束都传成同一个索引。这个信号不发出,你改了数据界面也不会更新,这是新手最常犯的错误之一。另外第四个参数(roles)在新版本里支持传角色列表,只更新显示相关的角色,能减少不必要的重绘。

flags和setData必须成对理解:flags决定"能不能编辑",setData决定"编辑了怎么存"。只写flags不写setData,双击能进去但回车后值弹回去;只写setData不写flags,压根进不去编辑状态。两个缺一不可。

3.3 增删行的规范姿势:begin和end系列函数

在QList里插一条数据本身很简单,contacts.insert()一行搞定。但如果你只做这一步就发信号,程序会在某些情况下崩溃。原因是视图在收到"行数变了"的通知之前,需要先知道"我接下来要插入几行,从第几行开始",好提前做准备(比如调整滚动条、重新计算布局)。所以Qt强制你成对调用:先beginInsertRows,再改数据,最后endInsertRows。

bool TableModel::insertRows(int position, int rows, const QModelIndex &parent) { beginInsertRows(QModelIndex(), position, position + rows - 1); for (int row = 0; row < rows; ++row) contacts.insert(position, Contact()); endInsertRows(); return true; }

begin和end之间夹着的才是真正修改内部数据的代码,这个顺序不能错。我见过有人图省事先改数据再调begin,运行起来偶尔正常,但一旦配合代理模型或排序就会莫名其妙出错。道理也简单:代理模型在begin/end之间会锁定自己的映射表,你把顺序搞反,它的映射就乱了。

removeRows同理,beginRemoveRows→contacts.removeAt→endRemoveRows。删除时要从后往前删,或者每次删固定位置,否则删完一个后面的索引全错位。示例里一次只删一行,所以直接用removeAt(position)就行。

3.4 代理模型怎么把过滤和排序做得不脏数据

QSortFilterProxyModel的用法其实简单得让人意外。Address Book里,主窗口构造时创建一个代理,把TableModel设成它的源:

proxyModel = new QSortFilterProxyModel(this); proxyModel->setSourceModel(tableModel); proxyModel->setFilterKeyColumn(-1); // 所有列都参与过滤

setFilterKeyColumn(-1)这一行很有讲究。默认情况下,过滤只作用于第0列,也就是只能按名字过滤。设成-1表示所有列都参与——这样你搜地址里的关键词也能命中。如果你只想按某一列过滤,就把列号传进去。

过滤的触发就是搜索框的textChanged信号连到一个槽,槽里调proxyModel->setFilterFixedString(text)。FixedString是精确子串匹配,还有setFilterRegExp支持正则、setFilterWildcard支持通配符。排序则是proxyModel->sort(column, Qt::AscendingOrder),示例里可能没显式做,但只要你调用一次,后续插入的数据也会自动按规则排。

代理模型最妙的一点是它不复制数据。很多新手担心"套了代理会不会内存翻倍",完全不会。代理只是维护了一张"代理行号 → 源行号"的映射表。界面点击代理的第3行,代理通过映射表找到其实对应源模型的第8行,再回头去源模型要数据。过滤掉的行只是从映射表里拿掉了,源数据毫发无损。

提示:当你的数据既要显示又要修改时,setData会经由代理转发给源模型。所以你只要保证源模型的setData写对了,界面上的编辑就能正确落盘,代理不需要额外处理。

4. 从零复现并改造这个示例的完整过程

4.1 环境准备与.pro文件的配置要点

先说环境。你装好QT5.14.2之后,示例工程就在安装目录/5.14.2/Src/qtbase/examples/widgets/itemviews/addressbook或者Qt Creator的欢迎页Examples筛选栏里直接能找到。如果你想自己新建一个工程照着写,记得在.pro里加上QT += widgets。这个示例只依赖widgets模块,不需要network、sql之类的东西,工程很干净。

用Qt Creator打开后,注意addressbook.ui是用设计器画的,里面放了两个按钮、一个搜索框、一个QTableView。如果你习惯纯代码建界面,也可以把.ui去掉,在构造函数里手动new出这些控件再布局,效果一样。我个人建议两种都跑一遍:先用.ui版本感受一下设计器和代码怎么通过ui->指针连接,再自己纯代码写一遍,能加深对控件生命周期的理解。

编译前有个容易忽略的点:如果你从别处拷贝的代码,注意检查.pro里有没有CONFIG += c++11,因为示例用到了override关键字和列表初始化。QT5.14.2默认应该已经支持,但老工程升级过来可能要手动加。

4.2 关键步骤串讲:从添加联系人到落库

整个示例的数据流我按操作顺序帮你捋一遍。程序启动时,AddressBook构造函数里创建TableModel和QSortFilterProxyModel,把代理挂到QTableView上:

tableModel = new TableModel(this); proxyModel = new QSortFilterProxyModel(this); proxyModel->setSourceModel(tableModel); proxyModel->setFilterKeyColumn(-1); ui->tableView->setModel(proxyModel); ui->tableView->horizontalHeader()->setStretchLastSection(true);

点"Add"按钮触发addContact(),弹出AddDialog。这是个用设计器做的对话框,两个输入框加一个OK/Cancel。用户填完点OK,exec()返回QDialog::Accepted,主窗口就把对话框里的名字和地址取出:

if (dialog.exec() == QDialog::Accepted) { tableModel->insertRows(0, 1, QModelIndex()); QModelIndex index = tableModel->index(0, 0, QModelIndex()); tableModel->setData(index, dialog.name(), Qt::EditRole); index = tableModel->index(0, 1, QModelIndex()); tableModel->setData(index, dialog.address(), Qt::EditRole); }

注意这里的顺序:先insertRows把行腾出来,再用setData往对应格子里填内容。因为setData内部会去取contacts[index.row()],如果行还没插进去,这个索引就是越界的,程序立刻崩。这是新手最容易踩的坑,务必记住"先插行,再填值"。

删除稍微绕一点。因为视图上选中的是代理模型的索引,你得先把它映射回源索引才能删对地方:

QModelIndexList selections = ui->tableView->selectionModel()->selectedRows(); if (!selections.isEmpty()) { QModelIndex sourceIndex = proxyModel->mapToSource(selections.first()); tableModel->removeRows(sourceIndex.row(), 1, QModelIndex()); }

这个mapToSource是代理模型的必备操作。忘了映射,删的就是代理行号对应的源行号,一旦有过滤或排序,两者对不上,删错行是小事,索引越界崩溃是常事。

保存和读取就是标准的QFile+QTextStream。保存时QTextStream out(&file),遍历模型里的Contact,out << c.name << "," << c.address << "\\n"。读取时按行读,split(',')拆出字段,再走一遍"插行+setData"的流程。注意读取前最好先tableModel->removeRows(0, tableModel->rowCount())清空旧数据,不然新数据会追加在老数据后面。

4.3 自定义委托与对话框的配合使用

示例里编辑单元格用的是默认的行内编辑,但如果你想控制编辑时弹出的控件(比如地址想用多行文本框、名字想加下拉候选),就需要上自定义委托QItemDelegate。委托的思路是:重写createEditor决定用什么编辑控件,重写setEditorData把模型里的值喂给控件,重写setModelData把控件里的值写回模型。

QWidget *ContactDelegate::createEditor(QWidget *parent, ...) const { QLineEdit *editor = new QLineEdit(parent); return editor; } void ContactDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const { QString value = index.model()->data(index, Qt::EditRole).toString(); static_cast<QLineEdit*>(editor)->setText(value); } void ContactDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, ...) const { QLineEdit *lineEdit = static_cast<QLineEdit*>(editor); model->setData(index, lineEdit->text(), Qt::EditRole); }

写好之后调用ui->tableView->setItemDelegate(new ContactDelegate(this))挂上去。委托和模型是分工的:模型负责"数据长什么样",委托负责"数据在这格子上怎么被编辑和绘制"。Address Book的AddDialog其实可以理解成"整行级别"的编辑方式,而委托是"单元格级别"的编辑方式,两者不冲突,可以共存。

注意:委托的createEditor返回的控件内存由视图负责回收,你不需要手动delete,否则会双重释放。

5. 排查实录:那些年我在Address Book上踩过的坑

5.1 界面不刷新、数据改了看不见

这个问题几乎100%出在信号上。要么是setData里忘了emit dataChanged,要么是beginInsertRows/endInsertRows没配对。判断方法很简单:改完数据看表格是否更新。不更新,首先去setData函数里找有没有发信号;如果增删行不更新,去检查begin和end是否成对、顺序是否正确。还有个隐蔽情况——你改的是内部列表但绕过了setData,那模型当然不知道数据变了。任何对内部数据的修改都应该走模型的虚函数。

5.2 编辑后数据弹回原值

这个八成是flags里没加Qt::ItemIsEditable。结果就是能选中、能高亮,但双击进不了编辑态;或者进了编辑态但模型不接受新值(setData返回false)。还有一种可能是setData里角色判断写死了,比如只判了Qt::DisplayRole没判Qt::EditRole,视图提交的是EditRole,自然被拒。记住:显示用DisplayRole,编辑用EditRole,setData必须处理EditRole。

5.3 过滤后删除行删错了

这就是前面提到的忘记mapToSource。视图的选择模型返回的是代理索引,你不映射就直接拿行号去删源模型,过滤状态下映射关系是错位的,轻则删错行,重则越界崩溃。记住一个原则:只要是用户从视图上产生的索引,用之前都要先映射回源模型。反向操作(从源到代理)用mapFromSource。

5.4 CSV读取乱码或字段错位

乱码多半是编码问题,读写时统一指定UTF-8。字段错位则是地址里本身带了逗号,简单split(',')就拆错了。稳妥的做法是约定一个不会出现在数据里的分隔符(比如制表符\t),或者用QTextStream配合引号转义。示例为了简洁用的是逗号,实际项目里建议换成更安全的分隔方式。

5.5 常见问题速查表

现象最可能原因处理方式
表格完全空白rowCount返回0或模型没设给视图检查rowCount和setModel
双击无法编辑flags缺ItemIsEditable补上编辑标志位
改值后弹回setData未处理EditRole或未发信号补角色判断和dataChanged
增删行崩溃begin/end顺序错或未成对严格先begin改数据后end
过滤不受影响filterKeyColumn设错设为-1或目标列号
删除删错行未mapToSource选择索引先映射回源
排序结果不对数据角色未提供正确类型data里返回可比较的QVariant

5.6 几条压箱底的实操心得

第一条心得:从最简单的模型开始,先不要代理。我第一次做的时候一上来就把源、代理、视图全接上,出了错都不知道是哪一层的问题。后来改成先只用源模型跑通增删改,确认没问题再套代理加过滤,调试效率高了一倍。分层的好处是排查也分层。

第二条:善用qDebug()在data函数里打日志。视图调用data极其频繁,你在里面打印"谁在问哪一行哪一列什么角色",很快就能看清视图和模型的交互规律。等你把交互流程摸熟了,再把这些日志删掉。这个笨办法比看文档效率高得多。

第三条:改数据一律走公共接口。不要图方便在AddressBook里直接tableModel->contacts.append(...),哪怕你写了个public的getter。所有增删改都通过insertRows/setData/removeRows,让模型自己发信号。一旦绕过去,你就会遇到那种"数据明明改了界面就是不更新"的灵异现象。

第四条:测试用例要覆盖排序+过滤+编辑的组合场景。单看每个功能都正常,一叠加就容易出问题,尤其是过滤后编辑再清空过滤,索引映射错位的问题会集中暴露。多花十分钟把组合场景过一遍,比事后debug两小时划算。

6. 把Address Book的思路迁移到实际项目里

6.1 数据源换成数据库的思路

Address Book用QList<Contact>当数据源,实际项目里更常见的是数据库。迁移思路其实很直接:把TableModel内部的QList换成你查出来的结果集(比如QVector<Contact>),在首次加载时执行一次SELECT把数据取出来填充。如果数据量大到不能一次装下,可以在canFetchMore和fetchMore里做分页加载——视图滚到底部时会自动调这两个函数,你每次多查一页追加进去就行。这样即使表里有几十万行,界面也不会卡。

写入方向,setData里除了改内存,还要把变更同步到数据库,通常做法是标记"脏行",攒一批再一次性提交事务,比每改一格就发一条UPDATE快得多。删除同理,removeRows里记下要删的主键,统一执行。

6.2 从单表到主从表:一个模型喂多个视图

学会了TableModel,再往上走一步就是主从表(Master-Detail)。比如左边一个列表显示所有联系人,右边一个表格显示选中联系人的详细信息。你完全可以用同一个模型挂两个视图,通过QTableView和QListView共享QSortFilterProxyModel。当用户在列表里选中某一行,你在槽里拿到索引,映射到源模型,再驱动另一个视图显示相关细节。这就是Model/View架构最诱人的地方——数据只有一份,视图可以有无数个,彼此通过模型这个"真相来源"保持同步。

6.3 把过滤框升级成组合条件

示例里的过滤是单个QLineEdit,只能按一个关键词搜。想升级成"按名字搜+按部门筛+按时间范围"这种组合条件,靠setFilterFixedString就不够了。这时候可以自定义一个继承自QSortFilterProxyModel的类,重写filterAcceptsRow函数,在里面拿到每一行的数据,按你任意复杂的规则判断是否接受。这个函数返回true就显示,false就过滤掉。判断逻辑随便你写,多条件、正则、甚至调用外部接口都行,代理模型只认这个bool结果。

6.4 自定义绘制让表格更专业

默认的表格绘制比较朴素。如果你想给某些行上色(比如状态异常的标红)、给单元格加进度条或图标,就要自定义委托的paint函数。思路是:先调QStyledItemDelegate::paint画默认内容,再根据index.data(自定义Role)决定要不要叠加额外的装饰。你可以自己在模型里定义扩展角色,比如Qt::UserRole + 1代表状态,委托根据这个角色取色绘制。模型和委托通过自定义Role通信,是Qt里非常优雅的一种扩展方式。

我一直觉得,Address Book这个示例真正的价值不在于它实现了多少功能,而在于它把Model/View这套框架的最小完整闭环展示得清清楚楚。你把它吃透之后,再去写任何基于模型的数据界面,都会觉得有章可循,而不是对着一堆API瞎试。我自己后来的几个上位机项目,表格基类基本都是从这个示例改出来的,改来改去核心还是那几行beginInsertRows和dataChanged。如果你愿意花一个下午把源码逐行读一遍再自己动手敲一遍,收获绝对比看十篇概念文章来得实在。

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

Zephyr BSP: 43-BSP CI CD自动构建发布

摘要:本文讲解如何为 BSP(板级支持包)搭建完整的 CI/CD 流水线。核心思路是:Git push 触发分层 CI——先跑 Fast CI 快速反馈,再跑 Full BSP CI 覆盖 Build Matrix,最后用 Hardware CI 验证真实硬件;通过固定 Docker 构建环境、版本化 Toolchain、Kconfig/Devicetree 校…

作者头像 李华
网站建设 2026/9/29 3:38:15

2026座舱域控与车规芯片选型图谱:从架构到量产要点解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/29 3:37:47

AI工程实战:从零搭建稳定可靠的文档问答Agent系统

AI工程&#xff08;ai engineering&#xff09;这两个词放在一起&#xff0c;最近被讨论得越来越频繁。很多人以为它会提示词就能算懂AI工程&#xff0c;实际真正上手之后才会发现&#xff0c;提示词只是最表层的东西&#xff0c;背后还站着数据准备、结果稳定性、成本控制、效…

作者头像 李华
网站建设 2026/9/29 3:37:42

迪普防火墙安装调试实战:三步开局与五个排错技巧

简介&#xff1a;迪普防火墙安装调试步骤借鉴文档面向网络工程师、系统运维人员及防火墙初学者&#xff0c;旨在帮助读者系统掌握迪普防火墙从初始配置到安全策略启用的完整流程。文档以实际调试为主线&#xff0c;详细覆盖VLAN划分与接口IP设定、安全域规划、静态路由配置、DH…

作者头像 李华