news 2026/9/29 1:49:29

Qt 5.14.2 通讯录示例:Model/View 与教程双路线

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Qt 5.14.2 通讯录示例:Model/View 与教程双路线

Qt Creator 欢迎页左边那排按钮里,Examples 大概是最容易被忽略的一个。我见过不少人装完 Qt 5.14.2 之后,教程看了一堆,官方文档也翻过,却从没点开过那个示例列表——直到某天要给内部工具加一个通讯录模块,才发现 QT5.14.2 自带的 Examples 里就躺着一个叫 Address Book 的完整工程,从界面布局、信号槽到 Model/View 分层,一应俱全。它不是什么玩具:这套示例里其实有两条完全不同的实现路线,一条在 tutorials 目录下,按步骤拆成好几个小工程教你手写界面;另一条在 itemviews 目录里,用 QAbstractTableModel 把数据层和 QTableView 彻底解耦。把这两套吃透,基本等于把 Qt Widgets 里最核心的那套东西过了一遍。这篇文章我会把它们的目录位置、类结构、关键代码、我编译和改装时踩过的坑都摊开讲,适合刚配好 Qt5.14.2 环境想找个小项目练手的人,也适合平时写惯了拖控件、想搞清楚 Model/View 到底怎么落地的人。

1. 从 Qt Creator 的欢迎页点进去:Address Book 到底装在哪

1.1 用 Examples 按钮定位,比翻安装目录快得多

最省事的入口是 Qt Creator 的欢迎页。左边一列按钮里点Examples,顶上会出现一个搜索框,输入address book,列表会立刻过滤出几个同名条目。每个条目点开之后,右侧能看到这个示例的简介、它属于哪个模块,下面一行小字写着源码目录的绝对路径,还有一个Open按钮,点下去直接把工程加载到编辑器里。

之所以推荐用这个入口,是因为它能顺带帮你确认一件很关键的事:你到底装没装 Examples。很多人为了省空间,安装时只勾了编译器套件,Examples 和 Sources 两个组件都没选,结果就是欢迎页的 Examples 里空空如也,然后到处问“Qt5.14.2 下载完了怎么没有示例”。这种情况下不用重新装,打开安装目录下的维护工具,在组件列表里把 Examples 勾上补齐就行。离线安装包一般会把这些组件一起打包,但安装向导里依然需要你手动勾选,这一步很容易漏。

如果你更喜欢直接在文件管理器里找,路径规律是这样的:Windows 下在C:\Qt\Qt5.14.2\5.14.2\<套件名>\examples\widgets\下面,套件名可能是msvc2017_64、mingw73_64之类;Linux 下通常在~/Qt5.14.2/5.14.2/gcc_64/examples/widgets/;macOS 则是~/Qt5.14.2/5.14.2/clang_64/examples/widgets/。进去之后你会看到两个相关目录:tutorials/addressbook和itemviews/addressbook。

注意:不要直接在安装目录里编译示例。Qt 的安装目录在 Windows 上往往位于 Program Files 之下,写权限受限,构建产物还会污染原始源码。Qt Creator 默认开启影子构建,会把编译输出放到单独的构建目录,这是正确做法,别手动关掉它。

1.2 tutorials 和 itemviews 两套同名示例,差别比想象中大

同一本官方文档里出现两个叫 Address Book 的示例,很容易让人困惑。它们的定位完全不同,我在第一次接触时也是绕了一圈才理清楚。

对比项tutorials/addressbookitemviews/addressbook
组织形式按 part 拆成多个独立小工程单个完整工程
数据容器QMap<QString, QString>封装在自定义 Model 里
界面构建纯代码手写 QGridLayoutQTableView + Model
核心教学点布局、信号槽、状态机QAbstractTableModel、视图委托
适合谁刚上手 Widgets 的人想搞懂 Model/View 的人
代码量级每个 part 一两百行五六个类,结构更完整

tutorials 那一套的价值在于它把过程拆开了。你打开 part1,看到的就是一个只会显示两个输入框和几个按钮的窗口,什么功能都没有;part2 加了新增;往后依次加上下一条、编辑删除、查找、存盘。每一步只动一小块代码,出了问题很容易定位是哪一步引入的。itemviews 那一套则是逆向的,它一上来就是完整形态,但用一层 Model 把“数据长什么样”和“界面怎么显示”隔开了,代码看起来更抽象,却更接近真实项目的组织方式。

我个人的建议是:先跑 tutorials,把它当成理解 Qt 事件和信号槽的练习;等你能自己解释清楚为什么它用了 QMap 而不是 QList,再去看 itemviews,那时候 Model/View 的那些虚函数就不难理解了。

2. tutorials 那一套:界面是怎么一步步长出来的

2.1 part1 只干一件事——把控件摆到网格里

part1 的构造函数短得有点过分,但正是这种短,让你能看清 QGridLayout 的排布逻辑:

AddressBook::AddressBook(QWidget *parent) : QWidget(parent) { QLabel *nameLabel = new QLabel(tr("Name:")); QLineEdit *nameLine = new QLineEdit; QLabel *addressLabel = new QLabel(tr("Address:")); QTextEdit *addressText = new QTextEdit; QGridLayout *mainLayout = new QGridLayout; mainLayout->addWidget(nameLabel, 0, 0); mainLayout->addWidget(nameLine, 0, 1); mainLayout->addWidget(addressLabel, 1, 0, Qt::AlignTop); mainLayout->addWidget(addressText, 1, 1); setLayout(mainLayout); setWindowTitle(tr("Simple Address Book")); }

这段代码里有三个细节值得停下来想一想。第一,地址用的是QTextEdit而不是 QLineEdit,因为现实中的地址经常要换行,单行输入框装不下;第二,地址标签在 addWidget 时多传了一个Qt::AlignTop,原因是 QTextEdit 比一行文字高得多,不指定对齐方式的话标签会垂直居中,看起来像浮在输入框中间;第三,所有会被翻译的字符串都套了tr(),这不是可选项,而是 Qt 项目的硬性习惯,后期做多语言时你会庆幸当初没偷懒。

还有一点新手常犯错:所有控件都 new 出来了,但一个都没 delete。这不是内存泄漏,因为 Qt 的对象树会在父对象析构时自动清理子对象——当你把mainLayout通过setLayout交给 this 之后,布局和布局里的控件都挂到了 this 这棵树上。理解这套父子关系,是理解 Qt 内存管理的第一步。

2.2 part2 引入对话框,同时把数据结构定了下来

到了 part2,界面上多了一个 Add 按钮,点击之后弹出一个独立对话框让你填姓名和地址。这个对话框被单独放在adddialog.h/cpp里,继承自 QDialog,内部也是两个输入框加 OK/Cancel 两个按钮,用 QDialogButtonBox 收尾。关键调用是这一段:

void AddressBook::addContact() { AddDialog dialog(this); if (dialog.exec() == QDialog::Accepted) { QString name = dialog.getName(); QString address = dialog.getAddress(); if (!name.isEmpty() && !contacts.contains(name)) { contacts.insert(name, address); // 刷新界面显示 } } }

exec()是模态执行,它会阻塞当前函数直到对话框关闭,返回值告诉你用户是点了确认还是取消。这是模态对话框最直观的用法,代价是调用点会被“挂住”。如果你的对话框里要做耗时操作,记得改成open()加信号连接的异步方式,否则主界面会假死。

数据容器用的是QMap<QString, QString>,key 是姓名,value 是地址。选 QMap 而不是 QList,理由有三条:一是姓名天然唯一,用 map 可以直接靠contains()做重名校验;二是 QMap 内部按 key 排序,显示出来天然有序;三是它的迭代器在插入删除时行为可预期,后续做“上一条/下一条”导航时特别顺手。当然代价是查询复杂度虽然是对数级,但插入顺序被打乱了——如果你需要“最近添加的排最前”,那就得换 QList 或者自己维护一个顺序数组。

2.3 从导航到编辑删除,一个 Mode 枚举撑起了整个状态机

part3 加了 Previous 和 Next 两个按钮,靠 QMap 的迭代器在记录之间移动。QMap 的迭代器支持++和--,这一点比 QHash 友好得多,后者只有单向迭代器。代码里会保存一个当前的迭代器成员,每次移动后调用一个统一的刷新函数把内容写回输入框。

真正让这套代码变得“像样”的是 part4。它引入了这么一个小枚举:

enum Mode { Navigation, Adding, Editing };

然后所有按钮和输入框的启用状态,全部由一个updateInterface(Mode mode)函数统一控制。Navigation 模式下输入框只读,Add 和 Edit 可用;Adding 模式下输入框可写,Submit 和 Cancel 可用,其他按钮全禁用。这就是一个最朴素的界面状态机。我见过太多项目里按钮的setEnabled散落在十几个函数里,改一处忘一处,最后出现“正在编辑状态下还能点删除”这种尴尬。示例里这个写法虽然简单,但方向是对的:界面状态只在一处决定,其他地方都只是触发状态切换。

part5 加了查找对话框,思路是遍历 QMap,用QString::contains()做子串匹配,命中就跳转过去。part6 则补上了持久化,用 QFile 配合 QDataStream 把整个 map 写进二进制文件再读回来。整个 tutorial 走完,你手里就有了一个功能完整、能存盘的桌面通讯录,代码总量还不大,非常适合拿来当模板改。

3. itemviews 那一套:把数据从界面里彻底剥出来

3.1 TableModel 里那几个必须重写的虚函数

itemviews 版本的灵魂是一个继承自 QAbstractTableModel 的类,骨架大概长这样:

class TableModel : public QAbstractTableModel { Q_OBJECT public: explicit TableModel(QObject *parent = nullptr); int rowCount(const QModelIndex &parent = QModelIndex()) const override; int columnCount(const QModelIndex &parent = QModelIndex()) const override; QVariant data(const QModelIndex &index, int role) const override; QVariant headerData(int section, Qt::Orientation orientation, int role) const override; Qt::ItemFlags flags(const QModelIndex &index) const override; bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; bool insertRows(int row, int count, const QModelIndex &parent = QModelIndex()) override; bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()) override; private: QList<QPair<QString, QString>> m_contacts; // 姓名、地址 };

这七个函数是表格模型的“最小可用集”。rowCount返回数据条数,columnCount返回 2(姓名、地址),data根据 role 决定返回什么——Qt::DisplayRole决定显示什么,Qt::EditRole决定编辑时初始值是什么,两个角色通常返回同一个字符串。headerData负责把列头文字设成 “Name” 和 “Address”,注意要判断 orientation,否则行号那边也会被塞进去奇怪的文字。

flags是最容易被漏掉的一个。如果不在这里返回Qt::ItemIsEditable,表格就是只读的,你双击单元格没有任何反应,而setData也永远不会被调用——很多人改了半天 setData 没生效,问题就出在这里。示例里的写法一般是判断 index 合法后再或上编辑标志。

至于底层容器,示例用的是QList<QPair<QString, QString>>这类结构,意思是第一格存姓名、第二格存地址。不同小版本的字段命名可能略有出入,但思路是一致的:模型内部只关心数据结构,完全不关心它会被谁显示。这就是 Model/View 存在的意义。

3.2 beginInsertRows 和 endInsertRows 的顺序,是这套机制里最容易出人命的地方

在 Model/View 里增删数据,绝对不能直接改容器就完事,必须把修改动作夹在一对函数之间:

bool TableModel::insertRows(int row, int count, const QModelIndex &parent) { beginInsertRows(QModelIndex(), row, row + count - 1); for (int i = 0; i < count; ++i) m_contacts.insert(row, qMakePair(QString(), QString())); endInsertRows(); return true; }

beginInsertRows的作用是提前通知视图:“我马上要往这几行塞数据了,你先把内部的持久索引和选中状态收一收。”视图收到通知后会把相关缓存置为无效。等你改完容器,endInsertRows再告诉视图重新去问模型要数据。顺序反了、或者只调用其中一个,轻则界面显示错乱、选中行跳到别处,重则直接崩溃——因为视图手里握着指向已经失效位置的索引。

删除同理,用beginRemoveRows/endRemoveRows,而且必须先通知、后删除。修改已有内容则用dataChanged信号:

emit dataChanged(index, index, {Qt::DisplayRole, Qt::EditRole});

这里有个可以优化的点:如果你只改了一列,就不要把整行两个索引都发出去,只发被改那一列的 index,视图刷新的范围更小,大数据量下差别很明显。这个技巧在几十条记录的通讯录上看不出效果,但等你哪天要处理几万行日志,就会感谢当初多写的那一行。

3.3 界面层因此变得几乎没有逻辑

有了模型之后,主界面的组装就变得很干净:创建一个 QTableView,把模型塞进去,开个排序,加两个按钮,连上信号槽,收工。整个视图文件里看不到任何“数据存在哪”“怎么增删”之类的逻辑,它只负责把模型给的东西画出来把用户的操作转发回去。

示例里还做了一个挺贴心的小设计:当通讯录一条记录都没有时,不显示一张空荡荡的表格,而是显示一段提示文字加一个添加按钮作为引导页(对应类通常叫 NewAddressTab 之类)。这种空状态设计在实际产品里非常常见,但很多入门示例都会忽略。添加联系人的对话框则复用了和 tutorial 里类似的结构,收集完姓名和地址后,先insertRows插一行空记录,再通过setData把两个字段填进去,最后顺手选中新加的那一行并滚动到可见位置。

提示:如果新增之后界面没更新,先检查是不是只改了容器没调模型接口,或者忘记endInsertRows了。这两个原因占了新手问题的一大半。

4. 编译和运行这两个示例时,我踩过的那些坑

4.1 构建套件没配好,报错信息却完全看不懂

第一次打开示例直接点运行,最常见的报错是No valid kits found或者一堆 “Qt version is not properly installed”。这不是示例的问题,是 Qt Creator 没找到可用的编译器套件。解决办法是进工具 - 选项 - 构建和运行,看构建套件页签里每个套件前面是不是绿勾。如果 Qt 版本那一栏是空的,说明安装目录没有被自动识别,手动添加一下 qmake 路径即可。

还有一种情况是套件配好了,但打开示例时忘了选——Qt Creator 打开 .pro 文件后会弹出套件选择对话框,很多人随手点确定,结果用了一个错误的套件(比如在 Windows 上选了 gcc 的套件却装的只有 MSVC),编译时就会报一堆找不到头文件的错误。养成习惯:打开工程后先看一眼左下角的套件选择器和构建模式。

4.2 用 MSVC 编译时中文乱码,根因在源文件编码

如果你在示例基础上改代码,加了几行中文字符串,用 MSVC 编译出来可能全是乱码。原因是 MSVC 默认按照本地代码页(简体中文环境下是 GBK)去解析源文件,而 Qt Creator 保存的文件是 UTF-8。两种编码对不上,中文自然就废了。

解决办法是在 .pro 里加一段条件编译:

msvc { QMAKE_CXXFLAGS += /utf-8 }

/utf-8是 MSVC 2015 之后提供的选项,等于同时告诉编译器“源码是 UTF-8、执行字符集也用 UTF-8”。加上之后乱码基本就没了。如果你用的是 MinGW 套件,一般不会有这个问题,MinGW 默认按 UTF-8 处理。另外注意,QString::fromLocal8Bit和QStringLiteral在处理中文时的行为不同,写死了不带 tr() 的中文字面量时更要留意源文件编码。

4.3 改了代码却没生效,八成是跑错了目录

这个坑我自己踩过不止一次。示例的构建是影子构建,源码目录里的文件是原始的,编译产物在另一个构建目录里。如果你直接双击源码目录下那个陈旧的 exe,看到的永远是旧版本。还有一种更隐蔽的情况:tutorials 目录里有七八个 part,每个 part 的工程名和窗口标题都非常接近,你改的是 part3,运行的却是 part2,那自然是白忙一场。

我的习惯是改完代码后看两个东西:一是 Qt Creator 底部编译输出的最后一行有没有 “Finished”,二是运行窗口的标题栏。示例里每个 part 的窗口标题都不太一样,对一下就知道跑对没有。

4.4 高分屏下界面糊成一片

在 4K 显示器上跑这些老示例,会发现字体和控件边缘发虚。Qt 5.14 里高分屏缩放不是默认开启的,需要在 main 函数最开头显式打开:

int main(int argc, char *argv[]) { QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); QApplication app(argc, argv); // ... }

这两个属性必须在 QApplication 对象构造之前设置,写在构造之后是完全无效的,而且不会有任何报错提示你写错了。另外如果你手动加过QT_SCALE_FACTOR之类的环境变量做测试,记得清理掉,否则会和这里的设置叠加,界面会变得异常巨大。

5. 把示例改成真正能用的通讯录,还差哪几步

5.1 存盘方案怎么选:二进制、JSON 还是配置项

tutorial 最后用的是 QDataStream 写二进制,代码短、速度快,但有两个明显缺点:文件不可读,人想手动改一条记录都不行;跨版本读取有风险,如果写入和读取两端的 QDataStream 版本号不一致,可能读出垃圾数据甚至抛出异常。改良写法是写入时显式锁定版本号:

QFile file(path); if (!file.open(QIODevice::WriteOnly)) return; QDataStream out(&file); out.setVersion(QDataStream::Qt_5_14); // 固定版本,避免跨环境读不出来 out << quint32(data.size()); for (const auto &p : data) out << p.first << p.second;

对于通讯录这种量级的数据,我更倾向于用 JSON,可读性带来的调试便利远大于那点性能损失:

QJsonArray arr; for (const auto &p : data) arr.append(QJsonObject{{"name", p.first}, {"address", p.second}}); QJsonDocument doc(arr); file.write(doc.toJson(QJsonDocument::Indented));

三种方案的取舍大致如下:

方案优点缺点适合场景
QDataStream代码最少、体积小、读写快不可读、跨版本需锁版本号数据量大、只给程序自己看
JSON可读、跨语言、结构灵活体积偏大、需要解析配置化数据、需要人工查看
QSettings平台原生存储、无需管路径只适合小数据、不支持复杂结构窗口位置、最近打开等零星状态

我实际的做法是:通讯录主体用 JSON 存,窗口大小、上次打开的窗口位置这类零碎状态塞进 QSettings。两者各干各的事,不用纠结统一。

5.2 加一个搜索框,只需要一个代理模型

itemviews 版本的示例本身支持点击表头排序,但没有搜索。加搜索框不需要动模型一行代码,套一层 QSortFilterProxyModel 就行:

QSortFilterProxyModel *proxy = new QSortFilterProxyModel(this); proxy->setSourceModel(model); proxy->setFilterCaseSensitivity(Qt::CaseInsensitive); proxy->setFilterKeyColumn(-1); // 所有列都参与匹配 tableView->setModel(proxy); connect(searchEdit, &QLineEdit::textChanged, proxy, &QSortFilterProxyModel::setFilterFixedString);

这里最值得记的是setFilterKeyColumn(-1)。默认值只对第 0 列做过滤,也就是说用户搜地址里的内容会一条都搜不到;传 -1 表示所有列都参与匹配,这才是通讯录该有的行为。如果需求更复杂,比如“姓名必须开头匹配、地址只要包含就行”,那就得自己继承 QSortFilterProxyModel 重写filterAcceptsRow,在函数里拿到源模型的 index 自己判断。这套扩展路径是 Qt Model/View 体系里非常经典的一环,理解了代理模型夹在中间的位置,很多“模型和视图之间还想插一层逻辑”的需求就有标准答案了。

5.3 输入校验和删除确认,示例里都没有

官方示例为了让代码短,基本不做校验,也不做删除确认。但真实项目里这两件事必须补上。校验有两个层次:一是对话框层,在点击确认时判断姓名非空、格式合法,不合法就弹提示、不关对话框;二是模型层,在setData里判断 value 是否为空,为空直接返回 false。后一层更重要,因为别人可能绕过你的对话框直接操作模型。

删除确认则简单得多,在触发删除前插一个 QMessageBox::question,注意把默认按钮设成“取消”而不是“确定”,防止用户一路回车误删。这个细节看起来微不足道,但用小键盘快速操作时真的能救命。我还习惯在删除后把被删记录的姓名和地址拼成一句提示显示出来,用户至少知道刚才删掉的是谁。

6. 为什么我建议把 Qt 自带的 Examples 当成第一手资料

6.1 能编译、能运行、能调试,这三条就把大部分教程比下去了

网上讲 Qt 的文章多如牛毛,但真正能编译通过的完整工程并不多,很多代码片段是截取出来的,缺头文件、缺 .pro 配置、缺信号槽连接,抄下来跑不通,你甚至不知道是文章写错了还是自己环境有问题。Qt 自带的 Examples 不一样:它是和这个版本一起发布、一起做过编译验证的,你打开就能跑,跑起来就能下断点单步调试。

这个价值其实和 Unity 生态里那些官方示例工程是一个道理。比如做深度相机开发的人,会去翻 Azure Kinect 或者 Femto Bolt 的官方示例,官方一般也会提供一套 Unity 示例工程,让你先跑通再改。这类示例的定位都是“一个能跑通的骨架”,而不是一份可以直接上线的产品代码。区别在于,Qt 的示例是可以直接在 IDE 里点开源码逐行看的,连 .pro 里每一行是干什么都摆在那里,学习成本更低。

顺带说一句,阅读示例的顺序很重要。我的习惯是先原样跑一遍,看它到底长什么样;再从头单步走一遍构造流程,看清楚对象树是怎么搭起来的;然后故意改坏一行代码,看看会报什么错——比如把endInsertRows()注释掉会怎样。这一步看着有点自虐,但它能让你把“错误现象”和“错误原因”对上号,下次在真实项目里遇到同样报错,反应速度完全不一样。

6.2 别把示例当成产品代码,尤其是这几个方面

示例为了简洁,做了很多让步。比如数据全部放在内存里,退出即丢(tutorial 里有了存盘,但 itemviews 版本没有);没有做任何输入校验;没有考虑并发或者多线程;界面样式也是系统默认的,没有做美化;错误处理基本是“打不开文件就算了”,没有给出用户可感知的提示。这些让步在示例里是合理的,因为它的目的是教一个概念,不是交付一个产品。

所以拿示例改项目时,我一般会先列一张清单:哪些是我要保留的结构,哪些是必须补的功能,哪些是需要重写的部分。Model/View 那套分层结构基本可以原样保留,改的是模型内部的数据源(从内存容器换成数据库或网络请求),以及视图层的交互细节。tutorial 那套手写界面的方案则适合规模小、界面固定的工具窗口,一旦界面元素超过二三十个,手写布局就开始痛苦了,这时候老老实实上 .ui 文件配合 Designer 更划算。

我个人在几个内部小工具里都沿用了 itemviews 这套骨架,把底层容器换成 SQLite 之后,几千条记录照样流畅,因为视图只渲染可见区域,模型只在需要时被查询。这个特性是 Model/View 体系最被低估的优势——它天生就是为大数据量设计的,只是示例里的数据太少,你感受不到而已。

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

死区时间详解:防止直通短路的硬件安全阀

/* 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 1:48:52

视觉空间学习者:用图像思维解锁高效学习之路

/* 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 1:47:45

企业级AI大模型平台落地框架:从架构分层到生产避坑全指南

/* 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 1:47:32

STM32F103开发板入门实战:从硬件检查到工程模板与调试烧录

/* 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 1:47:13

Git克隆失败的5大根因与秒级修复方案

/* 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 1:47:12

STM32驱动步进电机的硬件时序与电流校准实战

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

作者头像 李华