news 2026/10/31 1:11:32

DSPack让Delphi 12.3轻松播放视频与采集摄像头

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
DSPack让Delphi 12.3轻松播放视频与采集摄像头

简介:DSPack v2.3.3 for Delphi XE12 是一套专为 Delphi 12.3 环境优化的多媒体开发控件包,面向使用 Object Pascal 构建音视频应用的开发者,可快速实现音频/视频播放、录制、捕获、编解码及设备管理等功能,适用于媒体播放器、录像软件、视频采集工具等项目的快速原型搭建。压缩包共 347 个文件,按功能大致分为源码工程(98 个 pas、43 个 dpr、19 个 dpk、23 个 dproj)、静态库与头文件(28 个 lib、14 个 h)、组件安装脚本(bat 系列)、图标资源(bmp、dcr)及说明文档(chm、txt)等类型,7z 压缩后仅 7.47MB,便于离线保存与多环境复用。目前已有 157 人学习使用,适合中高级 Delphi 程序员在 XE12 环境中直接集成,借助包内注册脚本与工程示例可快速完成控件安装与功能验证。压缩包内附的 dproj/groupproj 便于打开完整工程,reg.bat 可一键注册组件,配合 CHM 帮助文档,能显著降低 DSPack 的入门门槛,帮助开发者高效开发出具有专业音视频处理能力的 Windows 应用程序。

1. 拿到这包别着急装:DSPack 是 Delphi 世界里最老的 DirectShow 套装

当你翻控件收藏夹,看到“Delphi 12.3控件之 DSPack v2.3.3 for Delphi XE12.7z”这个压缩包名时,第一反应多半是把它当普通第三方控件,装完拖到窗体上就完事。但 DSPack 不是 DevExpress,也不是皮肤库,它是 Delphi 里封装 Windows DirectShow 多媒体框架的组件集,解决的是视频文件播放、摄像头采集、音频抓取和逐帧回调这一类需求。20 多年前它从 Delphi 5 时代一路活到现在,靠的是 Windows 一直没移除 DirectShow 这套 COM 接口。这篇笔记不吹它能替代 FFmpeg,只讲怎么在 Delphi 12.3 里把这个老包安全编译、装进 IDE、跑通最小示例,并把最容易翻车的地方提前列给你。适合正在学 Delphi 又恰好要维护多媒体项目的人,也适合想给老 VCL 程序补一个播放器的人。

2. 先看代沟再做移植:DSPack 2.3.3 的底层假设和 Delphi 12.3 的差异

2.1 老代码的三条主要假设:Delphi 5-7、Win32、ANSI

DSPack 2.3.3 的开发背景是 2004 年前后的 Delphi 5/6/7 加 DirectX 9 SDK。那个年代没有 UnicodeString,字符串默认是 AnsiString,Char 只有 1 个字节;指针固定 4 个字节;编写控件时不强制区分设计期和运行期包;编译器对类型转换的检查也比现在宽松得多。

三条初期假设里,最影响移植的是字符串长度。老示例里经常出现PChar(String)、StrPCopy、用Byte数组手动拼 WAV 头这类写法,它们在 32 位 ANSI 环境下工作得很好,放到 Delphi 12.3 里就要小心:String默认已经是 UnicodeString,Char是 2 字节,旧的PChar用法往往把宽字符串截断成单字节,中文路径和文件名乱码就先从这里冒出来。

第二条假设是平台。DSPack 2.3.3 的源码里有大量Integer和指针混用,比如把接口指针放进Longint再传出来的写法。32 位下Integer和指针都是 4 字节,能对上;一旦编译目标切成 Win64,NativeInt变成 8 字节,Integer还是 4 字节,轻则编译告警,重则运行时把栈踩坏。所以我把“只选 Win32 平台”定为第一条规则,后面第 3 章会具体操作,这不是保守,是这套代码的物理边界。

第三条假设是组件包格式。DSPack 2.3.3 自带的 .dpk 包基于老版 IDE 的 BPL 机制,里面可能还写着对vcl、rtl、designide的引用。Delphi 12.3 的包管理器能打开这些 .dpk,但编译选项、平台配置、版本号规则全变了,不能指望双击就装好。

2.2 DirectX 9 时代留下的设计:COM 和 FilterGraph 模型

DSPack 做的事情,是把 Windows SDK 里的 DirectShow COM 接口翻译成 Delphi 接口,再包一层 VCL 控件。DirectShow 的核心概念是“过滤器图”:一个媒体文件从源过滤器读出数据,经过解码过滤器处理格式,最后送到渲染过滤器播放。每个过滤器通过 Pin 连接,整张图由 FilterGraph 统一调度。

对应到 DSPack 控件上,就是这个表:

控件/类封装的 DirectShow 对象典型用途
TFilterGraphIGraphBuilder / IMediaControl / IMediaEventEx创建过滤器图、控制播放暂停、接收事件
TVideoWindowIVideoWindow把视频画面渲染到 VCL 窗口
TSampleGrabberISampleGrabber抓取每一帧原始数据,做截帧、分析
TDSCaptureICaptureGraphBuilder摄像头预览、录像
TDSAudioCapture音频采集接口采集麦克风输入
TFilterEnumeratorIFilterMapper2枚举系统已注册的 DirectShow 过滤器

这套 COM 模型的好处是能力边界很清晰:DSPack 不负责解码,解码器由操作系统注册表里的 DirectShow Filter 提供。Windows 10/11 自带 MP4、AVI、WMV 的基础分离器和解码器,所以最小播放器往往能跑起来。但碰上特殊编码格式,比如 H.265 或某些厂商的私有流,就要额外装 LAV Filters 这类第三方 DirectShow 包,这一点必须在选型前想清楚。

那为什么不用 FFmpeg?常见做法对比下来,FFmpeg 跨平台、编解码能力强,但要在 Delphi 里用通常得维护 C 库的加载和回调封装。DSPack 的优势是纯 Delphi 代码、直接复用系统 DirectShow、组件拖到窗体就能用,特别适合老项目里已经有 VCL 界面、只想快速补一个播放能力的情况。劣势也很明显:官方早已停止维护,跨平台无望,遇到系统没有对应 Filter 的格式就没辙。

说到这里,顺便回应一下关于 Delphi 和 C++ Builder 目前处境的讨论:社区活跃度确实不如 20 年前,但 Windows 自带的 DirectShow COM 接口不会因为 IDE 冷热而消失,DSPack 这类“老控件”反而成了很多人接手维护老旧多媒体项目时的最短路径。

2.3 Delphi 12.3 与“XE12”的命名渊源和新编译规则

标题里写的“for Delphi XE12”,其实就是指 Delphi 12 这一代产品。Delphi 从 8.0 之后启用 XE 编号,Delphi XE 对应 8、XE2 对应 9,一直到 XE8 对应 15;再往后改成年份加副版本号的命名,10.3 Rio、10.4 Sydney、11 Alexandria,到 12 就是 Athens。把 Delphi 12 叫成“XE12”是老开发者的口语习惯,压缩包作者沿用这个叫法,你心里知道是同一个东西就行。

Delphi 12.3 带来的实际编译差异,比命名差异更重要。第一是字符串类型全面转 Unicode 后,老代码里所有Length(PChar(...))、SizeOf(Char)相关的逻辑都可能得改;第二是编译器对过时语法给出更明确的告警,比如 W1000 系列,不影响编译但对新手有干扰;第三是包管理器的搜索路径变成了项目级配置,不再像老版本那样装一次全局生效。

所以正确的心态是:DSPack 2.3.3 不是“装上去就能用”的黑匣子,它更像一份需要重新编译的老源码。你拿到这个 7z 包,真正要做的第一步不是找安装程序,而是把源码目录规划好、把编译目标锁死在 Win32、把报错一条条排掉。下面第 3 章就是这个过程的完整落地步骤。

3. 在 Delphi 12.3 里编译安装 DSPack:最小可行步骤

3.1 解压与目录规划:先分清设计期包和运行期单元

我不建议把 7z 直接解压到 Delphi 的安装目录里去。DSPack 的源码、Demo、帮助文档搅在一起,混进 IDE 的 Library 路径后一旦要移除,找半天都找不干净。常见做法是单独建一个第三方库目录,和 IDE 本体隔离。

解压命令我习惯这么写,注意目录名避开中文和空格:

mkdir D:\Libs\DSPack233 cd D:\Libs\DSPack233 7z x "D:\Downloads\Delphi 12.3控件之DSPack v2.3.3 for Delphi XE12.7z" dir /b

这段命令先把库目录建好,再用 7-Zip 解压。dir /b是快速看下顶层文件清单。你会看到几个关键目录:src放源码单元,packages或dpk放包文件,demos放示例工程。包文件通常是DSPack.dpk这一类的名字,具体以你解压出来的为准。

解压完先做一件事:把包文件用文本编辑器打开,看看requires段引用了哪些包。

requires rtl, vcl;

这个文件结构是拿来说明用的。老版本 DPK 的requires一般只有rtl和vcl,这没问题;如果出现dcl、designide之类的设计期依赖,说明这个包同时包含设计期组件注册逻辑,安装时要单独处理。逻辑说明:DPK 的requires决定了编译出来的 BPL 依赖哪些运行时包,你在 Delphi 12.3 里看到的报错“Unable to find package xxx.bpi”多半就来自这里。

3.2 修正包配置:把 DSPack.dpk 升级成 Delphi 12.3 认识的版本

打开 IDE,菜单 File → Open,选中DSPack.dpk。Delphi 12.3 打开老 DPK 时可能会弹提示说缺少某个平台或某个包,不要慌,先做三步。

第一步,在项目管理器里把目标平台切到 Win32。如果找不到平台切换,右键包节点选择“属性”,在目标平台里确认是Win32,不是Win64,也不是Android。第二步,检查项目选项里的输出目录和 DCP 目录,建议分别设为D:\Libs\DSPack233\build\win32和D:\Libs\DSPack233\build\dcp。第三步,把源代码目录src加入 IDE 的 Library 搜索路径,这一步不要漏,否则编译时找不到DirectShow9.pas这类单元。

这三步做完,再尝试编译,通常会遇到两种报错。一种是找不到旧单元,比如DSPACK.pas引用了某个老路径下的文件,改一下uses路径即可。另一种是平台相关指令,在源码顶部加上这一句能消掉不少噪音:

{$WARN SYMBOL_PLATFORM OFF} {$WARN UNIT_PLATFORM OFF} {$WARN UNIT_DEPRECATED OFF}

这段指令不是修复逻辑问题,而是把 Delphi 12.3 对老代码的“平台/弃用”提示关掉,让编译输出的错误信息收敛到真正的语法错误上。参数说明:SYMBOL_PLATFORM关的是标记为 platform 的 API 的告警,UNIT_PLATFORM关的是对整个单元的告警。真正阻碍编译的错误,比如类型不匹配、未定义的方法,不会因为这三行消失,该改还得改。

3.3 编译并安装到 IDE 组件面板

平台切好、路径配好、告警关好,接下来就是编译和安装。在项目管理器里右键DSPack.dpk,先点 Compile。编译通过后,包被构建成运行期的 BPL 文件,这一步有时候会在 Output 目录生成.dcp和.bpl。如果 Compile 报错,回到源码里改,改完再 Compile,直到干净。

Compile 通过不代表装进 IDE 了。要在 IDE 里看到 DSPack 组件面板,还得做安装:

右键 DSPack.dpk -> Install

这个动作会把包注册进 IDE 的组件列表,设计期代码里那些Register过程会被执行,组件面板上会多出一个叫 DSPack 的页签,里面有 TFilterGraph、TVideoWindow、TSampleGrabber 等一大堆组件。安装成功后再看包节点,前面会带上一个已安装的标记。如果 Install 报错,通常是缺少设计期包引用,右键安装时选择“仅安装运行期”或检查requires里有没有designide,按报错提示补上即可。

整个安装过程的参数调整就三个关键点:Win32 平台、单独的 DCP 输出路径、Library 搜索路径里包含 src。只要这三个点对,绝大多数“装不上”的问题都被排除了一半。

3.4 用最小 VCL 工程验证装配

装完组件后,立刻验证,不要急着把所有控件往界面上拖。新建一个 VCL Forms 项目,默认平台也是 Win32,然后往窗体上拖一个TFilterGraph和TButton,双击按钮写一行代码:

procedure TForm1.Button1Click(Sender: TObject); begin Caption := 'DSPack installed on Delphi 12.3'; end;

这段代码的目的不是播放视频,而是验证两件事:第一,设计期能创建TFilterGraph组件,说明包安装成功;第二,运行期Create不报错,说明运行时 BPL 加载正常。编译通过、按钮点击后标题栏变化,组件装配这一步就算彻底过关。

4. 第一个能跑的示例:用 TFilterGraph 拉通播放,再用 TSampleGrabber 截帧

4.1 最小播放器:TFilterGraph + TVideoWindow 组合

验证完组件装配,可以写最常用的播放器模式。窗体上放一个TPanel作视频宿主,放两个按钮分别控制播放和停止,再加一个TEdit填文件路径。核心代码逻辑如下:

procedure TForm1.FormCreate(Sender: TObject); begin FGraph := TFilterGraph.Create(Self); FVideo := TVideoWindow.Create(Self); FVideo.FilterGraph := FGraph; FVideo.Parent := Panel1; FVideo.Align := alClient; end; procedure TForm1.BtnPlayClick(Sender: TObject); begin FGraph.Active := False; FGraph.ClearGraph; FGraph.Active := True; FGraph.RenderFile(Edit1.Text); FGraph.Play; end; procedure TForm1.BtnStopClick(Sender: TObject); begin FGraph.Stop; end;

这段代码里的要点不少。TFilterGraph.Create(Self)指定了窗体作为 Owner,释放窗体会顺带释放组件,避免手动 Free。TVideoWindow.FilterGraph := FGraph是关键桥接,视频窗口组件只有知道谁来管理图,才能把自己的视频窗口句柄交给视频渲染器。Active := False再ClearGraph是清掉上一次的过滤器图,防止重复播放时资源泄漏。最后RenderFile是 DSPack 封装的“一键建图”接口,它会根据文件名自动选择分离器、解码器和渲染器。

参数说明:RenderFile的入参是string,Delphi 12.3 下传中文路径也没问题;Play只是启动数据流,Pause和Stop分别对应暂停和停止。播放的进度事件通过TFilterGraph的消息机制回传,一般不用专门去轮询。

4.2 用 TSampleGrabber 从视频流里截一帧

播放跑通后,我的下一个需求通常是截帧。在 DSPack 里截帧靠TSampleGrabber,它插在解码器和渲染器之间,每一帧数据流过时触发回调。最小截帧代码如下:

procedure TForm1.Grabber1Buffer(Sender: TObject; SampleTime: Double; Buffer: Pointer; BufferLen: Integer); var P: PByteArray; begin P := Buffer; if Assigned(P) and (BufferLen > 0) then begin // 在这里把 P 里的像素数据拷贝到 TBitmap.ScanLine end; end;

Buffer是指向帧数据的指针,BufferLen是这个缓冲区的字节数,SampleTime是这一帧的时间戳。回调里最常见的是把P按 RGB24 格式解读,塞进TBitmap的ScanLine。注意Buffer的格式未必是 RGB24,它取决于你连接的媒体类型,截 BMP 时通常是 24 位,但不同解码器可能输出 YUY2 或 NV12,所以用之前要先检查媒体子类型。

这段回调代码不要在回调里直接操作 VCL 控件。DirectShow 的回调线程不是 VCL 主线程,直接改TImage或TLabel轻则闪烁,重则直接崩。常见做法是把帧数据拷进一个线程安全的缓冲队列,主线程用TTimer定时取出来显示,这样画面既流畅也不卡 UI。

要启用截帧,除了拖一个TSampleGrabber到窗体并挂到FGraph上,还要在运行时把 Grabber 作为过滤器插入图里。设置它等待连接、设置媒体类型、再让RenderFile自动建图,顺序一乱就抓不到画面。具体 API 名称在 v2.3.3 的 Demo 里能看到,照着 Demo 的事件回调签名写即可。

4.3 摄像头预览与设备枚举

播放文件之外,DSPack 另一个高频用途是摄像头预览。理论上只拖一个TDSCapture,设置FilterGraph和VideoWindow,再调用预览方法就能出画面。但实际工程里我不会这么做,因为摄像头设备多、分辨率杂,直接开预览容易出现黑屏或绿屏,得先枚举设备让用户选择。

枚举系统里已注册的 DirectShow 过滤器,可以用 DSPack 的过滤器枚举工具类,遍历结果把设备友好名放进TComboBox:

var Enumerator: TFilterEnumerator; I: Integer; begin Enumerator := TFilterEnumerator.Create(nil); try ComboBox1.Items.Clear; for I := 0 to Enumerator.Count - 1 do ComboBox1.Items.Add(Enumerator.Filters[I].FriendlyName); finally Enumerator.Free; end; end;

逻辑说明:TFilterEnumerator枚举的是注册表里所有 DirectShow Filter,不止摄像头,还包括解码器、音频渲染器等。所以枚举结果里会有很多名字,你要根据“视频输入设备”这类设备类目来筛选。Count是过滤器数量,Filters[I].FriendlyName是设备的人类可读名称,比如“USB Camera”或“Integrated Webcam”。

参数说明:摄像头预览之前,最好先通过设备属性页设置分辨率。DSPack 的设备属性页调用也是封装的,打开后是系统标准配置界面。选完摄像头型号、调好参数,再开始预览,成功率会高很多。如果你在 Win11 上遇到摄像头被系统隐私开关挡住,那是系统权限问题,跟 DSPack 无关,先检查“设置 → 隐私 → 相机”的访问开关。

4.4 参数怎么改:视频渲染模式和窗口管理

很多人在跑通播放器后想改视频窗口样式,比如让视频铺满 Panel、跟随缩放、做无边框播放。DSPack 的窗口管理建立在 DirectShow 的IVideoWindow上,常用的调整是窗口拉伸时让画面等比缩放,避免变形。

TVideoWindow有一个控制窗口模式的属性,老系统用 Overlay 模式,新系统推荐 VMR9 或更后面的渲染器。VMR9 模式支持窗口透明、纹理渲染等高级能力,但要求系统有 DirectX 9 运行时,Windows 7 之后的系统都内置没问题。如果播放时画面不显示但声音正常,优先怀疑渲染模式没配对。

调整渲染模式的常见写法是:

FGraph.VideoRenderer := rmVMR9;

这里的rmVMR9是枚举值,rmVideoRenderer是老模式,不同版本 DSPack 的枚举名有小差异,以你源码里搜VideoRenderer属性看到的定义为准。这个参数一定要在RenderFile之前设置,因为图一旦建好,渲染器就固定了,之后再换模式不会生效。

5. DSPack 避坑手册:装不上、闪退、黑屏和 Unicode 乱码

5.1 现象:编译 .dpk 时直接报错,提示找不到包或平台不匹配

原因:老 DPK 引用了早期 IDE 的运行时包名称,或者项目配置里默认平台是 Win64,DSPack 源码不兼容 64 位。

解决:在项目管理器里把包编译平台切成 Win32,并检查requires里的包引用是否存在于 Delphi 12.3 中;designide相关引用在纯粹运行期包阶段可以先注释掉,等安装时报错再补。如果源码里出现“无法解析的单元”,确认 Library 搜索路径已包含解压目录的 src 文件夹。

5.2 现象:编译过了,运行一创建 TFilterGraph 就报 COM 类未注册

原因:DirectShow 的 COM 对象没有注册,或者程序进程里没有初始化 COM 环境。VCL 主线程在部分配置下不会自动调用 CoInitialize,老项目里如果用了大量第三方控件屏蔽了初始化,问题更容易爆发。

解决:在项目主文件里、Application.Initialize 之前显式初始化 COM:

CoInitialize(nil); Application.Initialize;

这个操作要放在.dpr文件执行体最前面。逻辑说明:CoInitialize(nil)把当前线程初始化为单线程单元 STA,DirectShow 的 FilterGraph 主要运行在 STA 下,这正好匹配 VCL 主线程的需求。设置完再创建 TFilterGraph 就不会报“库未注册”或“未初始化的 COM 库”了。

5.3 现象:视频窗口黑屏,但声音正常

原因:视频渲染器没选对。老系统默认的 Video Renderer 与 VMR9 在窗口创建顺序上要求不同:窗口未创建完成就开始渲染,画面拿不到窗口句柄,只能出声音不见画。

解决:确认 TVideoWindow.Parent 在播放前已经可见且尺寸非零;把渲染模式显式设为 VMR9,并在 RenderFile 后再调用一次FVideo.SetVideoWindow重设宿主窗口。黑屏跟显卡驱动关系也很大,NVIDIA 驱动更新后偶发 Overlay 模式失效,换 VMR9 是最稳妥的绕行方案。

5.4 现象:中文文件名和路径乱码,视频根本打不开

原因:老源码某处把string当 ANSI 处理,文件名在传给 DirectShow 前被截断或编码错乱。DSPack 2.3.3 里不是所有方法都安全接受 UnicodeString。

解决:优先使用 Unicode 版本的调用,把文件路径显式转成WideString再传;如果乱码点是显示层,比如列表控件,做一次Utf8ToAnsi或者显示时用SysUtils.TEncoding转换。千万别用PChar(filename)去硬转,那个宏在 Delphi 12.3 下会把字符串转成 UTF-16 指针,方向不错,但老代码常常拿它再套一层AnsiString反而转回去,等于白转。

5.5 现象:TSampleGrabber 回调里操作 UI,界面直接卡死或闪退

原因:回调线程不是主线程。DirectShow 的数据流线程会在你的回调里执行像素拷贝,此时操作 VCL 控件相当于跨线程访问,轻则数据竞争,重则主线程死锁。

解决:回调里只做数据拷贝,把字节塞进TThreadedQueue<TBytes>或自己的循环缓冲,主线程用一个 30ms 的定时器去消费队列更新画面。如果一定要在回调里通知界面,用TThread.Synchronize或者投递自定义 Windows 消息到主窗体句柄。这也是我在多线程场景里反复踩过的坑,线程安全队列比任何锁都好用,锁一多,视频帧几十毫秒一帧的节奏瞬间就被拖垮。

6. 进阶验证与工程化收尾:把 DSPack 包进自己的体系里

6.1 用 DLL 把 DSPack 隔离出来,而不是所有页面都拖控件

当项目里有五六个窗体都要播放视频,我不建议每个窗体都拖一套组件。常见做法是写一个独立的 DLL,把 DSPack 的播放器封装成几个导出函数,界面层只负责给路径和拿句柄。这样既隔离了老控件的编译环境,也让非 DSPack 的模块保持干净。

一个最小的 DLL 封装截面大致是:

library DSEngine; uses DSPACK in 'D:\Libs\DSPack233\src\DSPACK.pas'; exports PlayFile, StopFile; var FGraph: TFilterGraph; procedure PlayFile(const AFileName: PWideChar); stdcall; begin if not Assigned(FGraph) then FGraph := TFilterGraph.Create(nil); FGraph.Active := False; FGraph.ClearGraph; FGraph.Active := True; FGraph.RenderFile(AFileName); FGraph.Play; end; procedure StopFile; stdcall; begin if Assigned(FGraph) then FGraph.Stop; end; end.

逻辑说明:PWideChar直接把 Unicode 文件名传给 DirectShow,绕开 ANSI 转换;stdcall保证跨语言调用时参数不出错。DLL 里没有窗体循环,TFilterGraph 的播放控制在独立线程里跑,停止时通过stdcall接口控制,够用但不适合做复杂交互。

6.2 三个验证点,我每次迁移完都跑一遍

我习惯做完 DLL 封装后在 Win10、Win11 各跑一组验证,第一是播放不同格式文件,MP4、AVI、WMV 各一个,确认系统自带分离器都能解;第二是用 Process Explorer 看有没有显示为已加载的旧版 DLL,排除路径冲突;第三是连续播放 30 个文件,记录内存占用是否持续上涨。

第三个验证最容易漏,因为 DSPack 的过滤器图如果没释放干净,每播放一个文件就多占几十 MB,几个小时过去程序就卡了。我会在每次 Stop 和重新 Play 之前严格ClearGraph。这个习惯是我接手一个监控项目时被线上内存报警逼出来的,后来所有播放器代码都按这个规范写。希望帮到你。

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

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

量产烧录一致性与校验:从方案设计到产线实操的避坑指南

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

作者头像 李华
网站建设 2026/10/31 1:10:24

STM32+W5500 UDP以太网通讯实战:SPI驱动、收发实现与排障

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

作者头像 李华
网站建设 2026/10/31 1:09:56

I2C设备调试全攻略:从时序分析到STM32实战排查

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

作者头像 李华
网站建设 2026/10/31 1:09:41

Webots仿真避障算法实战:从场景搭建到状态机优化

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

作者头像 李华
网站建设 2026/10/31 1:08:44

MCU抗EFT设计:电源、IO与时钟的三层防御体系

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

作者头像 李华
网站建设 2026/10/31 1:08:40

基于ESP32的WiFi+BLE双模智能家居一站式方案实践

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

作者头像 李华