
简介这套资源为 Borland C Builder 6 开发者提供可直接编译安装的 TPngImage 与 TGifImage 图像组件专门解决 BCB6 环境中原生 Image 控件无法加载 PNG、GIF 两种常见图片格式的问题适合在 Win32 桌面程序开发中需要扩展图像支持的人员使用。压缩包共 47 个文件、约 701KBpas/hpp 为源码obj/dcu 为编译产物bpk/dpk 为 Borland 组件包工程chm 为帮助文档res 为资源文件此外还有辅助的 Readme、cfg/dof 工程配置等整体结构清晰便于直接编译安装和查阅。资源已有 711 人学习下载。若目标是快速让 BCB6 的 Image 组件支持常见图像格式这套组件包是较直接的选择。作者对原始组件做了适配修改按包内说明编译两个 bpk 后将对应文件夹路径加入 library 目录拖入 Image 控件即可正常载入 PNG/GIF 图像包内还附 PngDelphi.chm 和 Readme.txt提供组件接口说明与安装指引适合边装边查能省去从零移植或四处搜集成品的麻烦让 BCB6 老项目也能快速获得 PNG/GIF 显示能力。 接手过 BCB6 老项目的兄弟大概率都碰上过这样一件事客户甩过来一套新的 UI 设计稿里面的图标全是 PNG 或者 GIF 动图结果你往TImage上一放直接报错或者显示空白。BCB6 这个宝刀不老的 IDE原生对 PNG 和 GIF 的支持约等于零TPngImage、TGifImage这两个组件就是过来填这个坑的。本文就围绕这两个组件把我在 BCB6 里折腾它们踩过的坑、验证过的方案、以及能直接抄的代码一次说完。适合还在维护 BCB6 老工程的开发、或者准备在老代码里加图片格式支持的兄弟参考。1. 问题根源BCB6 原生图像支持到底缺了什么1.1 TImage 只能兜住这些格式BCB6 的TImage底层是TPicture它支持的图形格式看着不少但掰着手指头数一下就这几样BMP、JPEG、ICO、WMF、EMF。日常做界面凑合能用但一碰到互联网时代常见的 PNG 和 GIF 就直接抓瞎。PNG 在当年也是刚流行起来的新锐格式半透明、alpha 通道这些概念对老 VCL 来说完全是另一个次元GIF 虽然历史更久但 VCL 里始终没有内置一个能解码 GIF 动图的组件。这里要特别说清楚一个容易混淆的地方很多资料里提到 Delphi 6 开始内置了TPngImage但这是 Delphi 的单元BCB6 并不能直接捡现成。Borland 当时交付的 C Builder 6 和 Delphi 6 虽然共享 VCL 源码但 C 这边没有把 PNG 相关的.pas编译进去也没有生成对应的.hpp头文件。所以你直接写#include PngImage.hpp编译器一定懵圈。这就是为什么我们需要单独把它们作为第三方组件加载进来。还有一个误区是“升级 IDE 不就行了”。兄弟能升级我早就升了。老工程里可能有几百个窗体、几十个自定义 VCL 组件、甚至还有第三方商业控件库换编译器的成本是能劝退老板的。所以现实点我们得在 BCB6 这个环境内解决问题TPngImage和TGifImage这类可安装组件是最务实的出路。1.2 为什么要用组件而不是自己造轮子有人会问加载 PNG/GIF 而已自己写个解码器不是也能干编码能力先放一边单说格式规范的复杂程度就够你喝一壶。PNG 涉及到 zlib 解压、CRC32 校验、各种 color type 的像素处理GIF 更别提LZW 解压算法、帧延迟、透明色、全局调色板和局部调色板切换这些细节全埋在规范里。自己写一遍调试两三个月起步功能还不一定全面。用现成的TPngImage和TGifImage组件本质上是把这些格式的解析逻辑封装成了 VCL 图形类对外暴露的接口很干净LoadFromFile、Draw、Assign跟TBitmap的用法几乎一样。这就是组件化带来的最大价值——你不需要关心 PNG 的 IDAT 块怎么解也不需要关心 GIF 的 LZW 码表怎么组织只需要把它当作一个会画图的 VCL 对象来用。再者这两个组件在当年是经过了大量 Delphi/BCB 开发者验证的性能、稳定性、边界情况的处理都比自己写靠谱得多。我见过有人非要手撸一个 GIF 解码器结果遇到隔行扫描的 GIF 动图就花屏最后还是老老实实回来用组件。别跟成熟方案过不去。2. 选型TPngImage 和 TGifImage 怎么拿到、版本怎么挑2.1 TPngImage 的来龙去脉TPngImage这个类最早来自 Gustavo H. Daud 写的 PNG Delphi Library后来被 Borland 收入 Delphi 6 的官方 VCL 中。社区里流传的版本很多从早期的 1.4 到 1.6 都有。BCB6 要用的版本我建议找 1.54 或 1.6 的源码包里面有PngImage.pas、PngLang.pas、PngMisc.pas这些文件。1.6 版本对 Delphi 7 的兼容性更好但在 BCB6 下直接用可能报一点小错误需要手动改源码里的个别条件编译段。1.54 反而跟 BCB6 更默契我自己的项目里最终定的就是 1.54。这里有个经验拿到源码包后先别急着装打开.pas文件头部看注释里的版本说明和兼容性列表。正规一点的组件包都会写清楚支持哪些 IDE。如果它明确写了 “Delphi 5/6/7, CBuilder 5/6”那 BCB6 基本没问题如果只写了 Delphi 7 以上你在 BCB6 里编译时大概率会遇到一两个“不认识的东西”。需要注意PngImage.pas依赖 zlib 库而 BCB6 自带 zlib 的支持所以这一层不需要额外引入。但个别精简版源码包会把 zlib 也一起打包路径配置不对时反而造成头文件冲突这个下面安装部分会细说。2.2 TGifImage 的来龙去脉TGifImage的经典实现来自 Anders Melander这哥们写了一个功能非常完整的 GIF 组件支持 GIF87a、GIF89a、动画播放、透明色、隔行扫描甚至还能生成 GIF。社区里流传以 9.x 版本居多BCB6 能用的是 9.2 或者更早的版本。源文件主要是GIFImage.pas依赖GIFCtrl.pas这类辅助单元整个包不大但功能极其扎实。为什么我强烈推荐用 Melander 的版本而不是社区里其他简化版因为动画支持这块他实现得非常 VCL 化。TGifImage继承自TGraphic可以直接赋给TImage的Picture提供Animate、AnimationSpeed、FrameCount、FrameIndex等一系列属性。而其他一些简化版只支持解码第一帧或者根本不支持动画在 BCB6 这种老环境里要自己补动画逻辑那工作量一下就上来了。选 Melander 版本的时候要留意源码里对编译器的判断。他的代码会读VER条件编译宏来适配不同版本的 VCL。BCB6 对应的VER宏是VER150和 Delphi 6 一样如果源码里只判断了.VER140以前的版本说明写代码的时候 BCB6 还没面世就需要在源码开头补一行{$IFDEF VER150}的适配逻辑。好在社区老手们早就把补丁打好了网上能下到的 BCB6 适配版本基本都处理过这个问题。2.3 版本与 BCB6 的兼容性判断关于兼容性我自己整理了一个判断思路非常管用看组件源码里的{$IFDEF}分支如果出现VER150、BCB、CPPBUILDER这类关键词说明作者覆盖过 BCB6 的场景。看依赖的 VCL 单元。PngImage.pas依赖的核心就是Graphics、Classes、SysUtils这些都是老 VCL 一直有的如果组件依赖了Vcl.Imaging.PngImage这种带命名空间的单元BCB6 就读不懂了这个版本肯定不能直接用于 BCB6。看官方或社区里有没有 BCB6 的预编译.dcu/.obj。如果有直接拿来用省事很多如果没有就要做好改源码的准备。一句话总结不要迷信“最新版”在 BCB6 这个环境里版本越新反而越危险因为新的开源代码会慢慢引入新语法特性老编译器完全不认。最稳的是找 2002-2004 年间发布的版本那批组件和 BCB6 是同一个时代的产物兼容性好到出奇。3. 安装细节把组件塞进 BCB6 的两种方式3.1 设计期安装装到组件面板第一种方式是设计期安装适合你想在窗体上像拖TButton一样拖组件来用。BCB6 的入口在菜单栏的Component - Install Component弹窗里有Into existing package和Into new package两个选项。如果你只是自己用建议新建一个包命名比如BCB6ImgAdds.bpk避免污染官方包。把PngImage.pas和GIFImage.pas都加进同一个包后点击编译。BCB6 会先编译这两个 Pascal 单元生成对应的.hpp头文件然后注册到组件面板。如果一切顺利打开组件面板你会看到一个叫Adds或你自己起名的页面里面躺着TPngImage和TGifImage两个组件。这里有个大坑编译包的路径中不能有中文和空格。很多人把组件包解压到C:\Program Files (x86)\BCB6Adds\结果编译报各种奇怪的错误其实 BCB6 的bcc32命令行处理带空格的路径很容易出问题。我一般会放到C:\BCB6Lib\PNGLib\、C:\BCB6Lib\GIFLib\这种纯英文短路径下一次通过。还有一点如果你以后给别人发代码对方机器没安装这两个组件设计期安装的窗体可能打不开。所以很多老项目其实更偏向于下面这种方式——运行期引用不注册到面板。3.2 运行期引用不装面板直接 include运行期引用是我个人用得最多的方式特别适合共享源码给别人维护的场景。思路很简单不注册到组件面板而是把编译生成的.hpp文件和.obj文件随项目走自己在代码里创建对象。操作分三步用 BCB6 打开一个空包把两个.pas加进去编译一次生成PngImage.hpp、GIFImage.hpp以及对应的.obj文件。把.hpp文件拷贝到项目的 include 路径下或者放到工程目录里。在要用到的地方#include PngImage.hpp/#include GIFImage.hpp然后就可以new TPngImage()了。如果编译报链接错误直接在.cpp文件顶部加一句#pragma link PngImage.obj和#pragma link GIFImage.obj指定链接目标文件。这种做法非常老派但胜在可控。我甚至见过有团队直接把这两个.obj和.hpp提交到 SVN 里后续开发者 checkout 下来就能编省去了每人配组件面板的麻烦。需要注意的一点是PngImage.hpp里会#include它自己依赖的头文件比如pngdecl.hpp、zlib.h。如果这些文件不在搜索路径上编译还是过不了。最省事的做法是把组件包源码目录加到 BCB6 的Project - Options - Directories/Conditionals - Include path里让它自己去找。3.3 路径与编译参数检查清单整理一个我自己每次装组件都会检查的清单照着一遍过组件源码目录纯英文、无空格比如D:\Libs\PngImage。Include path包含PngImage.pas所在目录确保.hpp里的依赖头文件能找到。Library path包含编译后.obj所在目录BCB6 链接时能搜到。Conditional defines如果源码里有{$DEFINE UsePNG}之类的开关需要在Project - Options - Directories/Conditionals - Conditional defines里对应填写。zlib 环境确认 BCB6 安装目录下的include里有zlib.hlib里有zlib.lib。通常默认有但如果你的 BCB6 是精简安装版可能缺这一条值得先看一眼。只要这份清单没疏漏编译期的问题能干掉八成。4. 实操加载 PNG/GIF 并处理透明与动画4.1 PNG 加载与透明显示的坑先说最简单的加载代码很直白#include PngImage.hpp void __fastcall TForm1::LoadPng() { TPngImage *png new TPngImage(); try { png-LoadFromFile(logo.png); Image1-Picture-Assign(png); } __finally { delete png; } }这段代码把 PNG 加载进来并显示到TImage上。注意我用的是Picture-Assign(png)而不是把TPngImage转成TBitmap再赋值。为什么因为TPngImage重写了Draw方法在绘制时会正确处理 alpha 通道。如果你先转成TBitmap半透明像素会全部怼成实底透明白边变白底、圆角图标变方角那效果简直灾难。如果你想把 PNG 画到自定义的Canvas上直接用Draw方法png-Draw(Canvas, Rect(10, 10, 100, 100));它内部会识别当前Canvas是否为TImage的画布自动走 alpha 混合路径。TPngImage 对 alpha 的处理相当细腻半透明渐变、抗锯齿边缘都能保持住。再强调一个资源问题TPngImage加载大图时内存占用并不低它内部展开的图像数据是原始像素大小。处理完及时delete不要长期挂在内存里。上面代码用了try...__finally就是为了确保不管哪个环节出错对象都能释放。4.2 GIF 加载与动画驱动GIF 静态加载和 PNG 一样简单#include GIFImage.hpp void __fastcall TForm1::LoadGif() { TGifImage *gif new TGifImage(); try { gif-LoadFromFile(animation.gif); Image1-Picture-Assign(gif); } __finally { delete gif; } }但很多人在这一步发现GIF 动图显示出来了却是第一帧压根不动。这不是组件坏了而是你还没告诉它“动起来”。Melander 的TGifImage动画播放机制是这样的Animate属性控制是否启用动画模式组件内部会跟踪当前帧和延迟时间但真正触发重绘还是要靠外部的消息循环刷新。换句话说动画驱动需要你给它一个“心跳”。我最常用的做法是配合一个TTimer// FormCreate gif-Animate true; Timer1-Interval 100; Timer1-Enabled true; // Timer1Timer void __fastcall TForm1::Timer1Timer(TObject *Sender) { if (gif gif-Animate) Image1-Repaint(); }TGifImage内部会根据 GIF 文件里的帧延迟信息自动推进FrameIndexImage1-Repaint()负责把当前帧画到屏幕上。Timer 的间隔不需要设成和帧延迟完全一致100 毫秒级别的刷新足够平滑。我实测过绝大多数 GIF 动图在这套驱动下播放都很流畅CPU 占用也不高。如果你要手动精准控制当前帧可以操作FrameIndex属性gif-Animate false; // 暂停自动播放 gif-FrameIndex 3; // 手动跳到第4帧 Image1-Repaint();这个特性在“点击按钮切换 GIF 表情”这种交互场景里非常实用。TGifImage还有一个AnimationSpeed属性默认值是 100表示按原速播放数值越大播放越慢。如果客户说动图“太快了”你可以调这个值这比改文件本身要方便得多。4.3 与 TBitmap 互转的常见陷阱有时候你不想直接显示图片而是要把 PNG/GIF 的内容拿去处理比如做缩略图、转成 BMP 存到数据库这时候就要跟TBitmap打交道。PNG 转TBitmap的坑我前面提过了直接展开会丢 alpha。稳妥的做法是手工在画布上绘制Graphics::TBitmap *bmp new Graphics::TBitmap(); try { bmp-Width png-Width; bmp-Height png-Height; bmp-Canvas-Draw(0, 0, png); // 后续用 bmp 做处理 } __finally { delete bmp; }这样画出来的TBitmap是带透明通道效果的结果虽然你拿到的还是 24 位 BMP但像素已经是 alpha 混合后的视觉效果不会出现白底。GIF 转TBitmap则要区分场景。如果 GIF 是静态的直接取当前帧画到 Bitmap 即可如果是动图你只能转当前帧想要“整段 GIF”变成动画 Bitmap 是不现实的。所以转 BMP 前先想清楚你要的是哪一帧。还有一个插曲GIF 的透明色是“索引透明”只有 1 个颜色被标记为透明PNG 是 alpha 通道透明每个像素有 256 级透明度。两者视觉效果完全不同。页面上的 GIF 透明图标转成 PNG 后边缘发硬PNG 半透明阴影转成 GIF 后直接变成不透明块。这个差异不是你代码的问题是格式本身决定的别在这上面纠结太久。5. 常见问题与排查实录5.1 找不到头文件/链接错误现象#include PngImage.hpp编译报错Unable to open include file或者链接报Unresolved external。原因头文件找不到说明.hpp的搜索路径没配上链接错误说明.obj或者对应的包库.bpi没有参与链接。排查步骤检查Project - Options - Directories/Conditionals - Include path是否包含组件源码目录。检查Library path是否包含编译产物目录。如果是新建包的方式先确认包已经编译成功再检查当前项目是否引用了这个包Project - Options - Packages里把自定义包勾上。运行期引用则确认#pragma link PngImage.obj路径写对了。这一组问题占了安装失败案例的一半基本就是路径没配齐。5.2 动画只显示第一帧现象GIF 显示出来了但纹丝不动。原因Animate没开或者没有定时刷新。解决办法把gif-Animate true;设置好然后用TTimer定时调用Image1-Repaint()。如果你开了Animate还是不动检查是不是在加载完之后又重新Assign了一次把动画对象搞丢了。还有一点TTimer的Interval设得太小比如小于 10ms会影响性能太大比如 500ms看起来会卡顿100ms 左右最折中。5.3 透明背景变成黑底或白底现象PNG 图标加载到界面上原来透明的区域显示成黑色或者白色。原因黑底把TPngImage转成TBitmap后丢失 alpha黑色区域是原来 alpha 归零的像素直接暴露出来。白底转TBitmap时像素被填充成了白色底再合成。解决办法不要再转 Bitmap直接Picture-Assign(png)或者用Canvas-Draw手动绘制。如果一定要转 Bitmap就用我第 4 节里提到的“画布绘制法”不要用Assign硬转。5.4 组件装不上源码编译报错现象把.pas文件加入包编译时报Undeclared identifier或者The address of something cannot be taken。原因组件的源码版本和 BCB6 编译器不兼容常见于高版本组件用了一些老编译器不认识的新语法。解决办法换旧版本组件包或者手动修改报错处的代码。Undeclared identifier经常是因为条件编译分支没走对看看{$IFDEF}前面的定义必要时在源码顶部手动补{$IFDEF VER150}{$DEFINE BCB}{$ENDIF}这类设置。改的时候慢一点逐行确认改完保存重新编译。还有一类比较隐蔽组件包里同时有PngImage.pas和zlib.pas而 BCB6 的 RTL 里也有 zlib 相关单元两个包的符号冲突了。这种时候不要自己编译 zlib改用 BCB6 自带的zlib.lib删除组件包里的 zlib 相关.pas文件即可。6. 一些经验建议我在 BCB6 上用这对组件折腾了大概两年最后形成了一条操作铁律跟界面显示直接相关的图片用组件加载省心且效果最稳跟文件格式转换、批量处理相关的功能优先考虑 GDI 直接解码代码反而更简洁。BCB6 下初始化 GDI 无非是GdiplusStartup一次调用但好处是它能读 PNG、JPG、GIF 甚至 TIF一个接口全搞定不用装一堆组件。组件方案真正的优势是它完美融入了 VCL 的TGraphic体系Assign、Draw、LoadFromFile这些用法天然统一写代码的手感非常顺。最后再分享一个小技巧如果你要把这两个组件提供的功能封装给团队其他人用别让每个人都自己去 include 头文件和配路径。做一个小工具类比如TImageLoader内部封装TPngImage和TGifImage的创建、加载、释放对外只暴露一个LoadToImage(const String FileName, TImage* Dest)方法。这样团队成员只管调用装不装组件、路径怎么配根本不用关心。老项目维护把“麻烦”集中在一个地方消化掉永远比让所有人一起踩坑要强。本文还有配套的精品资源点击获取