简介这份资源面向使用 VC2008 与 MFC 开发桌面程序的初中级开发者聚焦多选项卡界面这一常见需求通过 CTabSheet 类演示如何在单窗口内组织多个可切换页面。包内共 23 个文件以 8 个 h 头文件与 6 个 cpp 源文件为主体承载 TabSheet 封装、子页面对话框及主对话框的实现代码另含 sln 解决方案、vcproj 工程文件、rc 资源脚本、ico 图标与编译好的 exe 可执行文件压缩包约 200KB结构紧凑便于直接打开工程对照阅读。已有 309 人学习下载。示例完整呈现了从派生 CTabSheet 页面类、重写 OnInitDialog 与消息映射到用 CPropertySheet 的 AddPage 挂载页面并 DoModal 显示的流程读者可据此掌握选项卡页面的创建、标题设置与交互消息处理并在此基础上扩展出更复杂的多页窗口布局。1. 一个老工程里翻出来的 CTabSheet为什么它至今还值得拆接手一个维护多年的 MFC 项目时最容易被忽略的不是业务逻辑而是界面骨架。某次改一个配置对话框需求是把原来平铺的七八组参数塞进分页里我第一反应是上 MFC 自带的 CTabCtrl结果写完发现切页要自己手动 ShowWindow 一堆子对话框控件 ID 对不上就整页空白调试半天全是玄学。后来在工程目录里翻到一个叫 CTabSheet 的类几行代码就把「一个 Tab 页对应一个子对话框」这件事封装干净了才意识到这类老派封装的价值。这篇就围绕 VC2008 环境下用 CTabSheet 做窗口 tab 的完整例子展开讲清它解决什么问题、工程怎么组织、核心代码长什么样以及拿到源码后怎么改成自己的多页界面。适合正在维护 MFC 老项目、或者需要快速给对话框加分页的开发者。2. 工程结构与 CTabSheet 的定位它到底封装了哪一层2.1 先看清 MFC 原生 Tab 的痛点MFC 的 CTabCtrl 本质上只是一个「标签条」控件它负责画标签、响应切换但不负责页面内容。原生用法要求开发者自己准备 N 个子对话框或子窗口在 OnTcnSelchange 里根据当前选中项把对应的子窗口 ShowWindow(SW_SHOW)、其余隐藏还要处理子窗口的定位、父窗口关系、创建时机。这套流程写一次还行写三次就开始复制粘贴改一个页面顺序要动好几处索引属于典型的血泪经验来源。CTabSheet 这个类的思路很直接继承 CTabCtrl内部维护一个子对话框指针数组对外暴露 AddPage 和 ShowPage 两个动作把「创建子页、挂到 Tab 上、按索引显示」这条链路收进类里。调用方只需要在父对话框的 OnInitDialog 里把每个页面对应的对话框类实例加进去剩下的切换逻辑由类自己完成。这种封装在 VC2008 时代非常常见因为那时候没有现成的属性页框架那么顺手而 CPropertySheet 又偏「向导式」样式受限。2.2 典型工程目录长这样拿到这份例子目录结构一般不会太复杂核心就是几个文件加一个演示对话框。下面是我按常见组织方式还原的结构实际文件名可能略有差异但职责划分基本一致TabSheetDemo/ ├── TabSheetDemo.sln # VC2008 解决方案文件 ├── TabSheetDemo/ │ ├── TabSheetDemo.h/.cpp # 主对话框承载 CTabSheet 控件 │ ├── TabSheetDemo.rc # 资源脚本含主对话框与各子页对话框模板 │ ├── resource.h # 控件 ID 定义 │ ├── CTabSheet.h/.cpp # 核心封装类继承 CTabCtrl │ ├── PageBasic.h/.cpp # 子页一基础参数 │ ├── PageAdvanced.h/.cpp # 子页二高级选项 │ └── PageAbout.h/.cpp # 子页三关于/信息页 └── res/ └── TabSheetDemo.ico这个结构的关键点在于每个子页都是一个独立的 CDialog 派生类有自己的 .rc 对话框模板和控件 IDCTabSheet 不关心页面里有什么只负责把页面窗口挂到标签条下面。主对话框里放一个 CTabCtrl 占位控件通过 DDX 绑定成 CTabSheet 类型需要把控件变量类型改成 CTabSheet或者在代码里 SubclassDlgItem。2.3 类之间的关系与消息流向理解 CTabSheet 要先理清三条关系。第一条是继承关系CTabSheet 继承 CTabCtrl所以它天然拥有标签条的绘制和点击通知能力。第二条是聚合关系它内部持有子对话框指针的数组常见做法是CDialog* m_pPages[MAXPAGE]加一个计数页面生命周期由外部或类内部管理。第三条是父子窗口关系每个子对话框创建时以 Tab 控件所在对话框为父创建后移动到标签条下方的矩形区域。消息流向是这样的用户点击标签CTabCtrl 发出 TCN_SELCHANGE 通知CTabSheet 的 OnTcnSelchange 处理函数拿到当前选中索引隐藏旧页面、显示新页面。整个过程对主对话框透明主对话框不需要写任何切换代码。这种「控件自己管自己」的设计在 MFC 里算是比较干净的封装方式也是它比裸用 CTabCtrl 省事的地方。提示子页对话框创建时一定要用无边框、无标题的样式否则会出现两层窗口边框叠在一起视觉上很别扭。3. 核心代码拆解AddPage 与页面切换的完整链路3.1 头文件里的成员设计先看 CTabSheet 的类声明这是理解整个封装的入口。下面这段是按常见实现还原的声明字段命名可能不同但结构一致// CTabSheet.h #pragma once #define MAXPAGE 8 // 最多支持的页数按需调整 class CTabSheet : public CTabCtrl { DECLARE_DYNAMIC(CTabSheet) public: CTabSheet(); virtual ~CTabSheet(); // 添加一页传入子对话框指针和标签文字 BOOL AddPage(CDialog* pPage, LPCTSTR lpszTitle); // 按索引显示某页 void ShowPage(int nIndex); // 当前选中页索引 int GetCurPage() const { return m_nCurPage; } protected: virtual void PreSubclassWindow(); afx_msg void OnTcnSelchange(NMHDR* pNMHDR, LRESULT* pResult); DECLARE_MESSAGE_MAP() private: CDialog* m_pPages[MAXPAGE]; // 子页指针数组 int m_nCount; // 已添加页数 int m_nCurPage; // 当前页索引 CRect m_rcPage; // 页面显示区域 };这里有几个设计点值得说。MAXPAGE 用宏定义固定上限是老代码的典型做法简单但不够灵活二次开发时可以换成 CArray 或 std::vector。m_rcPage 缓存页面区域避免每次切换都重新算。m_nCurPage 记录当前索引ShowPage 里用它做边界判断。AddPage 返回 BOOL方便调用方判断是否超过上限。3.2 AddPage 的实现与参数含义AddPage 是整个类的入口它要做三件事插入标签项、创建子对话框、记录指针。下面这段实现展示了完整流程// CTabSheet.cpp BOOL CTabSheet::AddPage(CDialog* pPage, LPCTSTR lpszTitle) { if (m_nCount MAXPAGE || pPage NULL) return FALSE; // 1. 在标签条上插入一项 InsertItem(m_nCount, lpszTitle); // 2. 创建子对话框父窗口设为 Tab 所在对话框 // 样式去掉标题栏和边框做成嵌入页的样子 if (!pPage-Create(pPage-GetTemplateName(), this)) return FALSE; // 3. 记录指针并递增计数 m_pPages[m_nCount] pPage; m_nCount; // 4. 第一页默认显示 if (m_nCount 1) ShowPage(0); return TRUE; }逐段说明。InsertItem 的第二个参数是标签文字索引用 m_nCount保证顺序和添加顺序一致。Create 的第一个参数用 GetTemplateName() 拿到对话框模板名这是 CDialog 的成员函数前提是子页类用 IDD 构造第二个参数传 this让子对话框以 Tab 控件为父窗口这样它才会显示在标签条区域内。样式方面子页对话框模板在 .rc 里要设置成 Child 和无边框否则 Create 出来的窗口会带标题栏。这里有个容易翻车的点如果子页对话框模板的 Style 没设成 ChildCreate 会失败或者显示异常。我一般会在资源编辑器里把子页对话框的 Style 改成 Child、Border 改成 None然后在代码里不再额外改样式保持模板和代码一致。3.3 ShowPage 与 OnTcnSelchange 的配合页面切换的核心在 ShowPage它负责隐藏旧页、显示新页、调整位置。OnTcnSelchange 只是把通知转成索引调用void CTabSheet::ShowPage(int nIndex) { if (nIndex 0 || nIndex m_nCount) return; // 隐藏所有页 for (int i 0; i m_nCount; i) { if (m_pPages[i] ! NULL m_pPages[i]-GetSafeHwnd() ! NULL) m_pPages[i]-ShowWindow(SW_HIDE); } // 显示目标页并移动到标签条下方 CDialog* pPage m_pPages[nIndex]; if (pPage ! NULL pPage-GetSafeHwnd() ! NULL) { pPage-ShowWindow(SW_SHOW); pPage-MoveWindow(m_rcPage); } m_nCurPage nIndex; SetCurSel(nIndex); // 同步标签条选中状态 } void CTabSheet::OnTcnSelchange(NMHDR* pNMHDR, LRESULT* pResult) { int nIndex GetCurSel(); if (nIndex ! -1) ShowPage(nIndex); *pResult 0; }ShowPage 先遍历隐藏所有页再显示目标页并 MoveWindow 到 m_rcPage。m_rcPage 的计算通常在 PreSubclassWindow 或 OnSize 里完成取 Tab 控件的客户区去掉标签条高度。SetCurSel 用来同步标签条的高亮状态避免代码调用 ShowPage 时标签没跟着变。OnTcnSelchange 里 GetCurSel 拿到的就是用户点击的索引直接转给 ShowPage 即可。参数方面nIndex 是页索引从 0 开始和 AddPage 的添加顺序一致。m_rcPage 的典型算法是GetClientRect 拿到 Tab 控件客户区然后 AdjustRect(FALSE, m_rcPage) 让 MFC 帮忙扣掉标签条占用的高度这个 API 比手动算像素靠谱得多。3.4 主对话框里怎么用起来主对话框的 OnInitDialog 是调用方视角代码很短BOOL CTabSheetDemoDlg::OnInitDialog() { CDialog::OnInitDialog(); // 把占位控件绑定成 CTabSheet 类型 m_tab.SubclassDlgItem(IDC_TAB_PLACEHOLDER, this); // 依次添加三个子页 m_tab.AddPage(m_pageBasic, _T(基础参数)); m_tab.AddPage(m_pageAdvanced, _T(高级选项)); m_tab.AddPage(m_pageAbout, _T(关于)); return TRUE; }m_tab 是 CTabSheet 类型的成员变量m_pageBasic 等是各子页对话框类的成员实例。SubclassDlgItem 把资源里的 Tab 控件子类化成 CTabSheet这样它才能响应自己的消息映射。AddPage 顺序决定标签顺序第一页会自动显示。整个主对话框不需要写任何切换逻辑职责划分很清楚。注意子页成员变量要在主对话框里以对象形式持有不要用 new 后忘记 delete否则关闭对话框时子页窗口句柄泄漏。4. 从零跑起来VC2008 环境下的编译与调试步骤4.1 打开工程与依赖检查拿到源码后第一步不是急着编译而是确认工程配置。VC2008 的解决方案文件是 .sln双击用 Visual Studio 2008 打开。打开后先看解决方案资源管理器里有没有缺失的文件显示为黄色感叹号常见缺失是 .rc 里引用的图标或位图。然后检查项目属性里的「字符集」设置老工程可能是多字节新环境默认 Unicode不一致会导致 _T 宏和字符串函数报错。依赖方面这个例子只依赖 MFC 和标准 Windows 库不需要额外第三方库。项目属性里「使用 MFC」要设成「在共享 DLL 中使用 MFC」或「在静态库中使用 MFC」两者选其一和你的部署方式匹配。如果编译报 afxwin.h 找不到说明 MFC 组件没装需要回到安装程序里补上。4.2 编译与常见报错处理编译命令在 IDE 里就是 F7但命令行方式更利于排查# 在 VS2008 命令提示符下执行 devenv TabSheetDemo.sln /Build Debug|Win32如果报error C2065: m_tab : undeclared identifier说明主对话框头文件里没加 CTabSheet 成员或者没 include CTabSheet.h。如果报LNK2019: unresolved external symbol多半是 CTabSheet.cpp 没加入工程检查解决方案里该文件是否在「源文件」节点下。如果运行起来标签能点但页面不显示回到 3.2 节检查子页模板的 Child 样式。调试时我一般会在 ShowPage 里下断点看 nIndex 和 m_rcPage 的值。m_rcPage 如果是全零或负数说明 AdjustRect 没调用或调用时机不对页面会被 MoveWindow 到屏幕外表现为「切了但看不见」。4.3 运行效果与验证清单跑起来后应该看到主对话框里一个标签条三个标签可点击点击后下方区域切换到对应子页内容子页里可以正常操作控件。验证清单如下检查项预期表现异常时看哪里标签显示三个标签文字正确、顺序对AddPage 调用顺序、InsertItem 参数页面切换点击标签内容跟着变OnTcnSelchange、ShowPage页面位置内容在标签条正下方m_rcPage 计算、AdjustRect子页控件按钮/编辑框可操作子页模板 Style 是否 Child关闭对话框无崩溃、无句柄泄漏子页对象生命周期这张表基本覆盖了初次运行会遇到的几类问题按顺序排查能省不少时间。5. 避坑与排查CTabSheet 用起来最容易翻车的几处5.1 子页显示成独立窗口或带边框现象点击标签后子页内容弹出一个带标题栏的独立窗口或者页面区域出现双层边框。原因子页对话框模板的 Style 没设成 Child或者 Border 没设成 None。解决在资源编辑器里打开子页对话框模板属性里 Style 选 Child、Border 选 None保存后重新编译。如果代码里用 Create 时传了样式参数确保没有覆盖模板设置。5.2 切换页面后旧页控件还在响应现象切到第二页后第一页的按钮仍然能被 Tab 键聚焦或者快捷键还能触发旧页逻辑。原因ShowWindow(SW_HIDE) 只是隐藏窗口子页对象仍然存在且消息循环可达。解决在 ShowPage 里对隐藏页调用 EnableWindow(FALSE)显示页调用 EnableWindow(TRUE)或者在子页的 OnSetActive 类回调里处理焦点。更彻底的做法是切换时把焦点 SetFocus 到新页的第一个控件。5.3 m_rcPage 计算时机不对导致页面错位现象对话框第一次打开时页面位置正确但窗口一拉伸页面就不跟着变或者初始就偏到一边。原因m_rcPage 只在 PreSubclassWindow 里算了一次没有响应 WM_SIZE。解决在主对话框的 OnSize 里调用 CTabSheet 的重新布局函数或者在 CTabSheet 里重写 OnSize重新 GetClientRect AdjustRect 后对当前页 MoveWindow。注意 OnSize 里要判断当前页指针非空。5.4 子页对象析构顺序引发崩溃现象关闭主对话框时偶尔崩溃调用栈指向子页的析构或窗口销毁。原因子页对象是主对话框的成员析构顺序和窗口销毁顺序不一致子页窗口句柄可能已被销毁但对象还在访问。解决在主对话框 OnDestroy 里先对所有子页调用 DestroyWindow再让成员自然析构或者把子页改成指针并在 OnDestroy 里统一 delete。关键是保证「窗口先销毁、对象后释放」。5.5 Unicode 与多字节混用导致标签乱码现象标签文字显示成乱码或问号。原因工程字符集设置和字符串字面量不匹配_T(基础参数) 在多字节工程里被当成 ANSI而资源里的对话框是 Unicode 模板。解决统一工程字符集VC2008 里建议全部用 Unicode如果必须多字节字符串用 CStringA 并确认 .rc 文件编码一致。改完记得清理解决方案重新生成避免旧的中间文件干扰。6. 二次开发改造把固定页数换成动态管理拿到这个例子后最常见的改造需求是「页数不固定」和「页面按权限动态加载」。原实现用 MAXPAGE 宏加固定数组改起来要动头文件不够灵活。我一般会做两步改造第一步把数组换成 CArrayCDialog*, CDialog*第二步把 AddPage 拆成「注册页面工厂」和「按需创建」两段这样权限控制就能插在创建之前。先看容器替换的核心改动// 改造后的成员用 CArray 替代固定数组 CArrayCDialog*, CDialog* m_arrPages; BOOL CTabSheet::AddPage(CDialog* pPage, LPCTSTR lpszTitle) { if (pPage NULL) return FALSE; InsertItem(m_arrPages.GetSize(), lpszTitle); if (!pPage-Create(pPage-GetTemplateName(), this)) return FALSE; m_arrPages.Add(pPage); if (m_arrPages.GetSize() 1) ShowPage(0); return TRUE; }ShowPage 里的循环改成遍历 m_arrPages边界判断用 GetSize()。这样页数上限只受内存限制添加顺序仍然决定标签顺序。改造后要回归验证三点第一页是否仍默认显示、切换索引是否和标签一致、关闭时是否所有页面都被销毁。验证方法是在 ShowPage 和析构里各打一条日志跑一遍增删页流程看日志顺序。第二步的动态加载思路是把「页面类」和「页面实例」分开主对话框先根据配置或权限决定要加载哪些页面标识再在 OnInitDialog 里按标识 new 出对应子页并 AddPage。这样权限不足的页面根本不会创建也就不存在隐藏后仍占资源的问题。改造时注意 new 出来的子页要在 OnDestroy 里统一 delete别和栈对象混用。还有一个实用技巧给 CTabSheet 加一个 SetPageTitle 方法内部调 SetItemText这样运行期可以改标签文字比如带未读数。加完之后在切换页时顺手刷新标题交互会自然很多。这类小改动不破坏原有结构适合在维护老项目时逐步推进。从那以后我每次给 MFC 对话框加分页都会先把子页模板的 Child 样式和 m_rcPage 的计算时机这两处强制走一遍检查再谈业务逻辑省下来的调试时间比写代码还多。希望帮到你。本文还有配套的精品资源点击获取
企业数字化 ERP 产品动态
相关推荐
MiMo2.6Pro端侧大模型实测:手机离线AI体验与翻车场景全记录 先交代一句背景:这台MiMo2.6Pro我前后测了两周多,不是拿三五个提示词跑一遍就下场写稿的玩票。“国模一哥”这个叫法最近在数码群里出现频率确实高,主要原因是这款模型不只在云端有大杯版本,还有能在手机本地跑的端侧版本… · 2026/11/2 17:07:54
测试工程师如何争取N+10裁员补偿:谈判策略与筹码运用 1. 为什么软件测试工程师能争取N10? 在互联网行业裁员潮中,测试工程师往往处于相对被动的位置。但根据我过去5年协助30测试同行谈判的经验,这个岗位其实具备独特的谈判优势。测试工程师日常工作中积累的项目文档、测试用例、质量报告等材料&a… · 2026/11/2 17:06:53
Postgres MCP Pro 实战:数据库健康检查与性能分析 1. 为什么我盯上了 Postgres MCP Pro 这个工具数据库健康检查这件事,做过运维和后台开发的人都懂,平时没人惦记它,一旦出事就是半夜被电话叫醒的级别。我手里管着几套 PostgreSQL 实例,规模不大但杂事不少:连接数有没有… · 2026/11/2 17:06:53
Gobot事件驱动架构实战:响应式机器人的事件流、时延与并发调优 做机器人项目最怕的从来不是电机不转,而是事件没到。我印象最深的一次调试,是一条本该在 20 毫秒内触发的碰撞回调,硬生生推迟了 400 毫秒才执行,排查了整整两天,最后发现是某个日志处理函数里塞了一次同步文件写入&am… · 2026/11/2 17:47:40
从零构建AI工程流水线:Python/Rust/TypeScript/Julia分层实践 1. 项目概述:从零构建AI工程能力,不是学AI模型,而是造AI流水线“ai-engineering-from-scratch”这个标题乍看像一门课程名,但真正懂行的人一眼就能看出它背后藏着的硬核信号——这不是教你怎么调用OpenAI API,也不是带… · 2026/11/2 17:47:40
OpenRig:铝型材开放式模拟赛车座舱DIY全记录 如果你也是那种会在深夜反复刷模拟赛车硬件视频的人,大概能懂我的处境:桌面上的折叠支架每次都要重新夹紧,液压踏板一踩,整张桌子跟着抖,玩个二十分钟就得停下来拧一遍螺丝。折腾了小半年后,我受够这些半吊… · 2026/11/2 17:47:40
场景法测试用例设计:从业务流程到异常流路径覆盖 上个月帮一个团队做测试用例评审,翻到一份下单模块的用例集,两百多条,全是"手机号必填""密码长度6到20位""验证码错误提示"这类字段级校验。我问了一句:用户领完优惠券、地址填错又重新改、支付超时… · 2026/11/2 17:47:40
C++课程设计:学生选课系统代码拆解与避坑指南 简介:学生选修课程系统设计是面向计算机及相关专业学生与C课程设计初学者的一份完整课程设计报告,围绕学生登录、课程浏览、选课与退选、个人信息管理、成绩查询和数据统计等功能展开,并覆盖需求分析、模块图与流程图、关键代码、调试分析、用… · 2026/11/2 17:47:40
前端大数据渲染优化:虚拟列表原理与实战 1. 当10万条数据砸向前端时会发生什么?第一次遇到这种需求时,我正喝着咖啡调试页面。后端同事突然说:"这个接口要返回10万条商品数据,你们前端处理下展示。"咖啡差点喷在屏幕上——这相当于让浏览器一次性加载一本300页… · 2026/11/2 17:46:39
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 4:42:45
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 5:17:18
雷达信号调制技术解析:从LFM到伪线性调频原理与应用 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 1:01:15
基于MAX14431与TM4C123的工业SPI隔离电路设计全攻略 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 1:01:15
PLFM_RADAR:相位线性调频雷达信号处理框架 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 1:01:15
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 4:42:45
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 5:17:18