简介:一份已在 Delphi 2010 中验证可用的 DSPack 2.34 组件包,专供使用 Delphi 2010 进行 Windows 应用开发的程序员使用。DSPack 是 Delphi 社区流行的组件集,涵盖网络通信、串口通信、多线程与数据库访问等常用功能,同时包含对 DirectShow 的封装,可辅助开发音视频播放、采集、服务器程序、实时监控系统和网络聊天室等应用。压缩包共 319 个文件,大小约 1.75MB,文件类型以 PAS 源代码为主体,并包含 DPK 组件包、DPR 工程、DFM 窗体、DCU 编译单元、BAT 脚本以及少量配置文档和图片资源,兼顾源码阅读、工程编译与组件注册清理等用途。目前已有 327 人学习下载,说明该资源受到同领域开发者的关注。该版本对 Delphi 2010 的 Unicode 特性做了适配,据资源提供者说明已在实际项目中测试,可降低自行移植旧版组件的兼容性风险;包内附带的示例工程(如播放器相关工程和滤镜注册脚本)能帮助开发者快速搭建原型,也可以作为研究 DSPack 组件结构和 DirectShow 调用方式的参考材料,适合正在学习 Delphi 组件开发或需要升级项目的工程师直接使用。
1. 为什么写着“已测试”的 DSPack 2.34,在 Delphi 2010 里一编译还是满屏报错
DSPack 2.34 大概是 Delphi 圈子里被转得最勤快的 DirectShow 封装包:过滤器图、视频窗口、音视频采集、抓帧全都有,代码风格直白,老工程师手里几乎都留了一份。可很多人拿同一个包在 Delphi 2010 里加载并编译,第一轮就崩出二三十个错误。问题不在包本身,而在 Delphi 2009 之后字符串体系整体换成了 Unicode,而 DSPack 的大量接口声明还停留在 ANSI 时代的 PChar 通行证,编译器自然不认。
这篇记录就是从解压开始,把包结构、Unicode 冲突点、改动顺序、编译安装、最小播放器验证完整走一遍。中间会专门列出我在这套组合下踩过的坑,按「现象 → 原因 → 解决」写清楚;最后再讲一条绕过 IDE 安装、直接用静态编译把 DSPack 源码并进工程的备用路线。适合正在 Delphi 2010 上维护老视频采集播放项目的人,也适合想把老代码从 Delphi 7 迁到 2010 的新手工程师。
2. 拆包排查:DSPack 2.34 的目录构成与 Delphi 2010 的字符串宽度冲突
2.1 这个包里都有什么:从解压目录就能看出设计年代
拿到手先解压,别急着打开 DPK。以我手头这份 DSPack 2.34 为例,顶层目录基本是这几类:Source 存放 DirectShow9.pas、DirectSound9.pas、DirectDraw9.pas、DSUtils.pas、DSIntf.pas 等绑定与工具单元;Packages 下是 DSPack.dpk 及相关依赖包;Samples 给出示例工程,适合当答案页对照;Bin 目录里一般会带几个旧 BPL/DCU,但这些通常来自旧编译器,不建议直接用。
看清目录后你能直接判断一件事:Bin 下的 BPL 是给老版本 Delphi 准备的,不同 IDE 版本的 BPL 不能混用。Delphi 2010 加载它会提示包版本不匹配,强行引入会导致 IDE 的包管理器混乱。因此唯一的出路是自己把源码重新编译成当前版本的 BPL。好处是顺手解决了路径不对、DCU 过期、条件编译选项不对等一连串潜在问题,坏处是第一次要耐心处理几处源码报错。
值得留意的是 Samples 目录。我一般会先把里面最小的那个播放器例子打开看一眼,看它 uses 了哪些单元、构造了哪些组件,这样后面自己建工程时能少走弯路。DSPack 不是只有一个大而全的单元,而是由 DirectShow9(COM 接口声明)、DSIntf(接口辅助)、DSUtils(公共函数)等多个文件拼起来的,直接往工程里拖整个 Source 目录是不可取的,按需引入才算正确姿势。
2.2 真正冲突的不是 DirectX,而是字符串宽度
很多人以为 DSPack 在 Delphi 2010 编译失败是 DirectX 版本问题,实际方向偏了。这套库封装的 DirectShow 接口本身在系统层很稳定,DSPack 用 Pascal 重新声明了 COM 接口、GUID、过滤器图状态等,编译错误几乎都集中在字符串类型上。
Delphi 7 时代的 string 是 AnsiString,PChar 是 PAnsiChar;到了 Delphi 2010,string 变成 UnicodeString,PChar 变成 PWideChar。DSPack 源码里大量用 PChar 作为通用的字符串承载类型,在不同编译器下语义完全不同。
| 编译器 | string 的真实类型 | PChar 的真实类型 |
|---|---|---|
| Delphi 7 | AnsiString | PAnsiChar |
| Delphi 2010 | UnicodeString | PWideChar |
一个典型场景是 PIN_INFO 这类结构体里的 TCHAR 数组。DirectShow 的 C++ 头文件在编译时按 TCHAR 的宽窄展开,DSPack 的 Pascal 声明却可能固定成 Char 数组。Delphi 7 下 Char 是单字节,恰好对上;Delphi 2010 下 Char 成了宽字节,而 DSPack 内部用来拷贝它的另一块缓冲还按单字节处理,于是Incompatible types一个接一个冒出来。看错误列表会发现,报错的单元高度集中在 DirectShow9.pas、DSUtils.pas 这两个文件里。
由此可以得出一个少被提及的结论:不是要把整个 DSPack 的字符串体系推倒重来,而是只需把「字符串相关的操作」统一到一个可移植的封装上,让它在 Ansi 和 Unicode 两种编译器下都走同一条逻辑。DirectShow 的 COM 接口本身大量使用宽字符,Delphi 2010 反而更接近原生习惯,只是老库的历史包袱让编译器先闹一阵。
2.3 动手前环境检查:三个必须先满足的前提
环境干净,后面才少些玄学。第一,Delphi 2010 尽量打全更新补丁,老 IDE 的包管理器在未更新状态下偶尔会在安装组件后丢失组件面板页面,这不是 DSPack 的问题,但会让你误判。第二,确认目标是 32 位,DSPack 2.34 没有 64 位适配,如果工程是 x64 编译目标,编译能过、运行也大概率崩,先切回 Win32 再说。第三,给当前 IDE 单独设置 BPL 输出目录,别用默认的 System32 也不要和 Delphi 7 共用同一个目录,两个版本的 BPL 名字相同但内容不兼容。
在 Tools > Options > Environment Options > Delphi Options > Library 里把输出目录改成一个专用文件夹,例如先建好一个 D:\DSPack2_34\BPL2010。这个动作能避免后面出现「装了 BPL 但 IDE 加载的是另一个版本」的诡异问题。目录名无所谓,关键是让当前 Delphi 版本的 BPL 落在不干扰其他 IDE 的位置,后面第四章最后一个坑还会回到这件事上。
3. 编译安装顺序:先打补丁再装 BPL,三个步骤把组件挂进 IDE
3.1 文本适配:给字符串操作做一个兼容层
不要一开始就全文搜索替换 PChar,那样会越改越乱。我建议先编译一次,让错误列表充当向导,集中处理真正的热点。通常第一轮报错集中在两处:旧代码里直接写StrPas(p)把 PAnsiChar 转 string;以及直接把 Char 数组赋值给 string 的地方。
针对第一类问题,我在 DSUtils.pas 末尾加了一个兼容函数,改动最小且不破坏原库结构:
{$IFDEF UNICODE} function DSPackStrPas(const p: PAnsiChar): string; begin Result := string(p); // PAnsiChar -> UnicodeString,编译期自动转换 end; {$ELSE} function DSPackStrPas(const p: PChar): string; begin Result := StrPas(p); end; {$ENDIF}这个函数的逻辑很直白:在 Delphi 2010 下,DSPack 很多接口返回的仍是 Ansi 风格缓冲区,需要显式指定 PAnsiChar 再交给 string,让编译器完成代码页转换;在 Delphi 7 下则回到原来的 StrPas。参数 p 的宽度必须写死,不能再用 PChar,这正是问题的核心。之后把编译报错的调用点从StrPas(...)改成DSPackStrPas(...)即可。注意这个函数名不要与 RTL 里现有函数冲突,所以我刻意用了带前缀的名字。
另一类典型问题是PChar(x)这种显式转换。在 Delphi 2010 里 PChar 已经是宽字符,遇到接口期望单字节字符串时就要把转换目标写成 PAnsiChar。这类改动每次影响一个调用点,我一般只处理编译器实际报错的位置,顺手记在记事本里,一轮下来一般三十到五十分钟能清完。真正耗时的不是改动本身,而是猜测哪些地方该保留 Unicode、哪些地方必须回到 Ansi——判断依据很简单:看它最终传给哪个 API。
3.2 编译 DPK:按依赖顺序来,先编译底层绑定
补丁打完后,打开 Packages 下的 DSPack.dpk。先别急着右键 Install,把 Project Manager 中 Requires 列表完整看一遍,里面出现某个包但还没编译时,先单独编译它。常见顺序是先编译 DirectDraw9、DirectSound9 这类紧贴系统的绑定包,再编译 DirectShow9 相关包,最后编译主包 DSPack.dpk。这个顺序不是硬性的,Delphi 会自动构建依赖,但先编译底层能让你在一开始就把错误集中在少数文件里。
编译动作在 Project Manager 窗口里右键当前包节点选择 Compile。成功后会生成 DCP 与 BPL 文件,位置就是 2.3 里设置的 BPL2010 目录。随后右键同一节点选 Install,把它注册进 IDE 的组件列表。在这里最容易犯的错是漏掉 Requires 里的子包,导致 Install 报错提示某个包缺失;解决方法是回头逐个编译,不要跳过。
如果 DPK 编译过程中还有报错,继续回到第 3.1 节的流程处理。报错清单里偶尔会遇到重复的 GUID 或接口方法重定义,这种通常说明 Source 目录里有同名旧 DCU 在干扰,用 Delphi 2010 的 Component > Clean up 清掉缓存后重编一次即可。
3.3 路径配置与组件面板验证
编译安装完成后,还要把源码目录挂进 IDE 的库路径。Tools > Options > Environment Options > Delphi Options > Library > Library path,添加一行:
D:\DSPack2_34\Source保存后新建一个 VCL Forms Application,在组件面板上查看是否多了 DSPack 页,里面能看到 TFilterGraph、TVideoWindow、TSampleGrabber、TFilter 等组件条目。如果组件面板没有刷新,尝试重启 IDE。这一步通过,说明注册成功,可以进入工程实战了。若在本步骤就失败,先看第四章,那里有几条更具体的排错记录。
4. 避坑记录:DSPack 2.34 在 Delphi 2010 下编译和运行的五个常见问题
4.1 编译时报 Cannot find file 'DSIntf.dcu'
- 现象:加载 DPK 后编译到某个单元时中断,提示找不到
DSIntf.dcu,有时还会跳出一串黄色感叹号。 - 原因:Library path 没有覆盖 Source 目录,编译器找不到 DCU 所在路径。另一种情况是之前残留的旧 DCU 存放在其他目录,编译器优先找到了错误版本。
- 解决:把 Source 目录加入 Library path,并用工具清理 DSPack 目录下所有 DCU/DCP,然后重新编译。不要手动去翻 DCU 文件路径,直接按 3.3 里的方式配置,重新编译会强制刷新。
4.2 Incompatible types: 'PChar' and 'PAnsiChar'
- 现象:在自写代码里调用
FilterGraph1.RenderFile('C:\test.avi')或给某个接口传入字符串参数时,编译器报类型不匹配。 - 原因:Delphi 2010 下字符串字面量是 UnicodeString,而老库的某个函数签名还是 Ansi 时代的声明,两边宽度不一致,编译器拒绝隐式转换。
- 解决:调用处显式指定预期宽度。如果 API 期望宽字符,就写
PWideChar(WideString(...));如果期望单字节字符,就写PAnsiChar(AnsiString(...))。我遇到过多次接口参数在 D7 下用 PChar 没问题,到 D2010 却需要改成 WideChar,全是因为老接口声明把宽窄混在了一起。
4.3 安装后组件面板里找不到 DSPack 页
- 现象:Compile 成功,Install 也提示成功,但新建窗体后组件面板没有任何新增页面。
- 原因:常见有两种,一是 BPL 输出路径不在 IDE 搜索范围里,二是 IDE 的组件包缓存没有刷新,注册信息没写进当前用户配置。
- 解决:先在 IDE 的 Components > Install Packages 中确认 BPL 是否出现在已安装列表;没出现就手动添加。添加后若面板仍旧空白,重启 IDE。还不能解决就把 BPL 从输出目录复制到系统模块目录后再执行一次添加,很多玄学问题最后都是靠这个动作解决的。
4.4 代码编译通过但运行期回调里 Access Violation
- 现象:播放器能启动,视频也有画面,但在 TSampleGrabber 的 OnBuffer 回调里访问字符串或 VCL 控件时直接崩溃。
- 原因:DirectShow 回调跑在流线程上,不是 VCL 主线程。Delphi 2010 下 UnicodeString 的内存管理与 VCL 控件访问都需要主线程上下文,直接操作就会踩内存。
- 解决:回调里不操作 string,也不操作窗体控件,只把原始缓冲指针和数据长度收下来,用
Move拷贝到预设缓冲区,再通过PostMessage把消息投递给窗体主线程处理。这样既保住了性能,也躲开了跨线程崩溃。抓帧回调里的完整处理见 5.3 节的写法。
4.5 同一台机器装 Delphi 7 与 Delphi 2010 时 BPL 相互污染
- 现象:在 Delphi 2010 里装好了 DSPack,回头打开 Delphi 7 时组件面板异常或提示 BPL 版本不兼容。
- 原因:两个版本编译出的 BPL 是同名文件,而系统库路径可能指向了同一目录,旧的把新的覆盖了,或者相反。
- 解决:从 2.3 开始就坚持让两个 IDE 使用各自的 BPL 输出目录,系统 PATH 里不同时加入两个目录。这套做法不仅对 DSPack 有效,所有 Delphi 第三方组件都适用。
提示:装这类老组件之前,先给 IDE 的配置目录做一次备份,出问题时能快速回滚,不用重新配置整个环境。
5. 最小工程验证:从新窗体到跑起第一个 DirectShow 播放器并抓帧
5.1 新建工程与引用准备
安装成功后,新建一个 VCL Forms Application,编译目标保持 Win32。先在窗体上拖入一个 TFilterGraph、一个 TVideoWindow、一个 TSampleGrabber,外加一个按钮,这是完整的播放控制组合。TFilterGraph 负责过滤器图的创建与运行,TVideoWindow 负责把视频渲染到窗口,TSampleGrabber 负责在流中间截取数据。
拖完组件后,把窗体的 BorderIcons 保留默认,无需改任何属性。接下来代码的关键点是:播放前先关闭过滤器图,清空旧图,再拉入文件;播放时再把 FilterGraph 属性接好,最后 Active 置 True。如果先设置 Active 再挂 VideoWindow,会出现画面黑屏但声音正常的翻车情况。
5.2 一个按钮播完一个视频
在按钮的 OnClick 里写下面这段代码:
procedure TForm1.btnPlayClick(Sender: TObject); var hr: HRESULT; begin FilterGraph1.Active := False; FilterGraph1.ClearGraph; hr := FilterGraph1.RenderFile('C:\tmp\sample.mkv'); if Failed(hr) then raise Exception.CreateFmt('RenderFile 失败: 0x%s', [IntToHex(hr, 8)]); VideoWindow1.FilterGraph := FilterGraph1; VideoWindow1.Visible := True; FilterGraph1.Active := True; end;这段代码的意图是:先把过滤器图停掉并清空,避免上一次播放残留的过滤器干扰;RenderFile返回 HRESULT,失败时打印十六进制错误码方便排查,常见的0x8004022b表示文件打开失败或格式不支持;succeeded之后再关联视频窗口并启动。注释里有个容易忽略的细节:如果你的 DSPack 版本里RenderFile参数是宽字符,直接传字符串即可;若编译报类型不匹配,参考 4.2 的显式转换方式。
ClearGraph 是 TFilterGraph 提供的清理方法,在播放多次时非常有用。不调用它,第二次播放可能出现滤镜复用的奇怪问题,比如画面尺寸变了、卡在暂停状态。Active 置 False 和 ClearGraph 的顺序不要反,先停图再清图,逻辑上更稳。
5.3 抓一帧存成 BMP:回调里的数据怎么搬出来
在窗体上双击 TSampleGrabber 的 OnBuffer 事件,把回调数据接收到自定义缓冲区。事件参数的名称以当前 DPK 编译后的声明为准,常见是 SampleTime、pBuf、BufferLen 三个。代码可以这样写:
procedure TForm1.SampleGrabber1Buffer(Sender: TObject; SampleTime: Double; pBuf: PByte; BufferLen: Integer); begin // 回调线程里不要碰 VCL 控件,只做内存拷贝 Move(pBuf^, FFrameBuffer^, BufferLen); PostMessage(Handle, WM_FRAME_READY, BufferLen, 0); end;在主线程的 WM_FRAME_READY 消息处理里再做 BMP 保存。下面这个函数把一张 RGB24 的帧写成文件,里面包含了 BMP 文件头、信息头和像素数据的完整拼装:
procedure SaveRgb24ToBmp(const AFileName: string; pBuf: PByte; AWidth, AHeight: Integer); var s: TFileStream; bf: BITMAPFILEHEADER; bi: BITMAPINFOHEADER; stride: Integer; begin stride := (AWidth * 3 + 3) and not 3; // 每行 4 字节对齐补位 ZeroMemory(@bf, SizeOf(bf)); bf.bfType := $4D42; // 'BM' bf.bfOffBits := SizeOf(bf) + SizeOf(bi); bf.bfSize := bf.bfOffBits + stride * AHeight; ZeroMemory(@bi, SizeOf(bi)); bi.biSize := SizeOf(bi); bi.biWidth := AWidth; bi.biHeight := -AHeight; // 负数表示 top-down,匹配多数视频帧 bi.biPlanes := 1; bi.biBitCount := 24; bi.biCompression := BI_RGB; s := TFileStream.Create(AFileName, fmCreate); try s.WriteBuffer(bf, SizeOf(bf)); s.WriteBuffer(bi, SizeOf(bi)); s.WriteBuffer(pBuf^, stride * AHeight); finally s.Free; end; end;这里最容易踩的坑是 stride 不一致。如果回调缓冲每行已经不是 4 字节对齐,整块写入会造成图像左右错位,此时必须逐行复制并补 padding。另一个坑是图像方向:很多 DirectShow 解码器输出的视频帧是 top-down,负高度值写出的 BMP 才是正像;如果存出来图像上下颠倒,把 AHeight 前的负号去掉就好。如果抓出来的画面偏绿或偏紫,说明 TSampleGrabber 协商出的媒体格式不是 RGB24,而是 YUY2 或 NV12,那需要先强制设置媒体类型。可以这样写:
var mt: TMediaType; begin ZeroMemory(@mt, SizeOf(mt)); mt.majortype := MEDIATYPE_Video; mt.subtype := MEDIASUBTYPE_RGB24; SampleGrabber1.SetMediaType(@mt); end;若你的版本把 SetMediaType 暴露成 MediaType 属性,改成赋值方式即可,语义相同。加了这个设置后,过滤图会在连接阶段优先协商 RGB24,抓帧省去很多颜色转换的麻烦。
6. 不装 IDE 的另一种用法:把 DSPack 源码静态并进工程并创建后台播放器
6.1 什么时候可以绕过 BPL 安装
如果只是某个小工具需要用到 DirectShow 的接口,不打算在窗体上拖组件,那么完全可以不装 DSPack 的 IDE 包,直接把 DirectShow9.pas、DSUtils.pas、DSIntf.pas 几个源文件加进工程的 uses 列表。这样做的最大好处是避免 BPL 污染 IDE,换机器编译时只要带着源码路径就能跑;代价是组件面板不可用,需要在运行时动态创建对象。
静态编译最适合后台任务:音频重采样、视频转封装、录制推流、取媒体时长等场景通常不需要视频窗口,纯粹依靠过滤器图跑通即可。对那些想快速出结果的应用,这比折腾 IDE 安装更省力,尤其是给同事交付代码时,对方不需要在自己的 IDE 里重复装一次包。
6.2 运行时创建 TFilterGraph 的最小代码
不依赖设计期组件,可以这样写:
function PlayInBackground(const AFileName: string): HRESULT; var FG: TFilterGraph; begin Result := E_FAIL; FG := TFilterGraph.Create(nil); try Result := FG.RenderFile(PWideChar(AFileName)); if Succeeded(Result) then begin FG.Active := True; // 在这里做媒体时长查询、音频采样等后台操作 Sleep(3000); FG.Active := False; end; finally FG.Free; end; end;这个函数的用意很明确:RenderFile 成功后不需要任何视频窗口,过滤器图会自动构建解码链路,音频会走系统默认输出,视频渲染部分则被丢弃,适合需要后台解码而不弹画面的场景。nil 作为 Owner 表示对象不随窗体释放,因此必须用 try-finally 手动 Free,防止触发资源泄漏。如果要做长时间的后台处理,建议把延时控制改成线程内等待,而不是在主线程里 Sleep,以免界面卡死。
从那以后,我在这类老 IDE 里装第三方组件,都会先在一个空白工程里把编译、安装、最小调用三步跑通,再把改过的源码单独备份一份。这样至少能确认组件本身没问题,后续排查范围不会扩大到环境配置上。DSPack 2.34 在 Delphi 2010 上其实完全可用,前提是接受它需要一点手工适配。希望这篇记录能帮你少走一段弯路。
本文还有配套的精品资源,点击获取