简介:Graphics32是一款面向Delphi开发者的高性能2D图形处理组件库,适用于图像编辑、数据可视化、游戏界面及多媒体等需要复杂渲染的场景。这份源码包来自其master分支,包含完整的Delphi单元、组件与示例工程,开发者既可直接集成,也能通过阅读源码理解硬件加速渲染、Alpha混合、图像滤镜、坐标变换等核心实现。包体共1853个文件,以pas单元源码、htm文档页、dpk/dproj工程配置为主,并附带png/gif等图形资源与dcu编译产物,整体仅4.1MB,结构紧凑,方便本地查阅和二次开发。已有240人学习浏览,适合具备一定Delphi基础、希望提升图形编程能力或研究开源架构的中高级开发者,可直接获取一套可编译、可扩展的图形组件库源码,减少从零搭建环境的时间。
1. 为什么 Graphics32 是 Delphi 图形组件库里的异类
大多数 Delphi 图形方案把TCanvas当作最终输出,Graphics32 却走了一条更底层的路:用内存中的TBitmap32像素缓冲承载所有绘制,TImage32这个 GraphicComponent 只是把这层缓冲呈现到窗口上。它把图形处理拆成一个个 delphiunit,每个单元负责一个渲染环节,所以无论是百万像素逐点处理、Alpha 混合还是旋转缩放,都比 GDI 硬画快一个数量级。图像编辑器、2D 游戏、科学可视化这类要频繁改像素的应用都是它的主场。新手可以用TImage32当强化版 TImage 用,熟手可以直接操作TBitmap32.Pixels定制滤镜链,库本身不挡路。
2. 从 DelphiUnit 到 GraphicComponent:Graphics32 的单元结构与组件骨架
2.1 先看 graphics32-master 目录里有什么
这个资源包是 master 分支里的工程子集,包含的脚本和精灵位图已经足够支撑一个完整的 2D 渲染链路。clean.bat和_clean.bat都是清理脚本,区别在于前者清理当前工程中间产物,后者清理范围更深,会把 packages 和 units 的缓存一起删掉;lazbuild.bat是 Lazarus 的构建入口;Gen.bat是生成器,常见做法是把内置配置或示例资源生成到对应目录。sprite1.bmp、sprite2.bmp、sprite3.bmp 是一组 RGB 主图,sprite1a.bmp、sprite2a.bmp、sprite3a.bmp 是对应的 Alpha 通道图,这种"彩色图加通道图"成对出现的方式,在 Graphics32 示例里很常见,目的是让开发者自己合成 32 位带透明通道的TBitmap32。
| 文件 | 在这个资源里的作用 | 什么时候跑 |
|---|---|---|
| clean.bat | 删除当前配置的 .dcu/.o 中间文件 | 切换编译器版本或发现旧缓存问题 |
| _clean.bat | 连 lpk 包缓存一起清理 | 包结构异常、链接到旧符号时 |
| Gen.bat | 生成资源或精灵数据 | 首次拉取代码后、编译之前 |
| lazbuild.bat | 调用 lazbuild 编译并注册包 | Lazarus 环境下构建 |
| sprite*.bmp / sprite*a.bmp | RGB 主图与对应 Alpha 通道图 | 合成 32 位 ARGB 位图时 |
提示:实际使用中不要跳过 Gen.bat。有些版本把GR32_System.res这类资源放在生成步骤里,目录里没看到 res 文件时,先执行一次Gen.bat再编译,否则会报资源找不到。
2.2 关键单元和组件定位
Graphics32 的源码按功能拆成若干单元,理解这些 delphiunit 的边界比死记 API 更能少踩坑。GR32 是最底层,定义了TColor32(AARRGGBB 格式的 32 位色值)、TPixel32、TBitmap32以及 D32 系列绘制后端;GR32_Image 提供可视化组件TImage32,它内部持有一个TBitmap32,设计期可以直接在窗体上放置并编辑;GR32_Resamplers 提供TLinearResampler、TCubicResampler、TKernelResampler等采样器,负责缩放旋转时的像素插值;GR32_Transforms 负责仿射变换和透视变换;GR32_Blend 和 GR32_Filters 则是混合算式与常用滤镜实现。
| 单元 | 关键类型 | 职责 |
|---|---|---|
| GR32 | TBitmap32, TColor32, TPixel32 | 像素缓冲、色值定义、基础绘制 |
| GR32_Image | TImage32, TPaintStage | 可视化控件、显示与重绘 |
| GR32_Resamplers | TLinearResampler, TKernelResampler | 缩放、旋转时的像素采样 |
| GR32_Transforms | TAffineTransformation | 仿射变换计算 |
| GR32_Blend | TCombineOp, TPixelCombineEvent | Alpha 混合与自定义混合回调 |
从 GraphicComponent 的角度看,TImage32才是组件面板上那个看得见的东西,而TBitmap32是组件背后真正干活的缓冲。很多入门代码把Image32.Bitmap.Pixels当普通数组直接改,这是允许的,但要注意TBitmap32默认是顶层低位格式,直接写Pixels不会自动做 Alpha 处理,需要透明叠加时得先设置DrawMode := dmBlend。
2.3 把单元和组件挂进 Delphi 工程
先让编译器和 IDE 认识源码,再在代码里使用组件。对于 Delphi 7 到 Community Edition 都适用。打开 Project 的 Delphi Compiler 搜索路径,把解压目录下的Source加进去;如果用的是 Lazarus,则通过lazbuild.bat或 IDE 直接加载Graphics32.lpk。随后在代码单元里写入:
uses Winapi.Windows, System.SysUtils, Vcl.Forms, GR32, GR32_Image, GR32_Resamplers; procedure TForm1.FormCreate(Sender: TObject); var Img: TImage32; begin Img := TImage32.Create(Self); Img.Parent := Self; Img.SetBounds(10, 10, 256, 256); Img.Bitmap.LoadFromFile('sprite1.bmp'); Img.Bitmap.DrawMode := dmOpaque; Img.Invalidate; end;这段代码先创建TImage32并绑定窗体作为所有者,再设置父容器和位置尺寸,然后LoadFromFile把 sprite1.bmp 读进内部的Bitmap缓冲。DrawMode := dmOpaque表示按不透明方式绘制,因为主图本身没有 Alpha 信息;最后Invalidate触发重绘。这里容易犯的错是没有把GR32_Image加进 uses,TImage32并不在 GR32 主单元里。参数层面,SetBounds的前两个参数是窗体坐标,后两个是宽高,256 的尺寸要和后续合成后的位图尺寸对应,否则控件绘制时会自动拉伸。
3. 构建与首次集成:基于 graphics32-master 的 Delphi 函数调用路径
3.1 构建脚本与命令行入口
拿到源码包,第一件事不是打开 IDE 拖控件,而是确认构建环境。lazbuild.bat里面常见的内容是:
@echo off lazbuild --add-package packages\Graphics32.lpk lazbuild -B src\Graphics32.lpk第一行把包注册进 Lazarus 组件库,第二行的-B表示全量 build,避免旧缓存造成的链接错误。如果只用 Delphi,可以跳过 lpk,直接把Source目录加进搜索路径即可;XE2 到 12.x 以及 Community Edition 的用法一致。换编译器版本后,中间的.dcu、.o、.ppu必须清掉,最典型的报错是 "Unit GR32 was compiled with a different version of System.Types",这时跑一遍clean.bat再重编。
Gen.bat负责生成资源,例如把 sprite 系列位图转换成 GR32 内部渲染所需的数据。执行顺序很重要,先 Gen 再编译,可以避开某些示例里 "Resource not found" 的坑。脚本输出目录通常和源文件目录相对路径绑定,不要把它单独挪到别处执行。
3.2 合成带 Alpha 的 TBitmap32
sprite1.bmp 和 sprite1a.bmp 成对出现,前者的蓝色通道对应透明值。在 Graphics32 里,透明信息不能像 PNG 那样自动附带在LoadFromFile结果里,所以需要手工把两张位图合成一张TBitmap32:
procedure MakeSpriteWithAlpha(const RgbName, AlphaName: string; OutBmp: TBitmap32); var RgbBmp, AlphaBmp: TBitmap32; X, Y: Integer; C: TColor32; A: Byte; begin RgbBmp := TBitmap32.Create; AlphaBmp := TBitmap32.Create; try RgbBmp.LoadFromFile(RgbName); AlphaBmp.LoadFromFile(AlphaName); if (RgbBmp.Width <> AlphaBmp.Width) or (RgbBmp.Height <> AlphaBmp.Height) then raise Exception.Create('RGB 与 Alpha 图尺寸不一致'); OutBmp.SetSize(RgbBmp.Width, RgbBmp.Height); for Y := 0 to RgbBmp.Height - 1 do for X := 0 to RgbBmp.Width - 1 do begin C := RgbBmp.Pixels[X, Y]; A := AlphaBmp.Pixels[X, Y] and $FF; // 取蓝色通道作为透明值 OutBmp.Pixels[X, Y] := (TColor32(A) shl 24) or (C and $FFFFFF); end; finally RgbBmp.Free; AlphaBmp.Free; end; end;Pixels[X, Y]返回TColor32,本质是 ARGB 顺序的 32 位无符号整数,所以A shl 24把 Alpha 放到最高字节。and $FF表示只取 Alpha 图蓝色通道,如果通道图本身是灰度,三个通道值相等,取哪个都一样。OutBmp.SetSize在设置宽高时会自动清空旧缓冲,避免数据残留。这个函数是精灵类项目里的标准工具函数,之后要缩放或旋转,直接对这个 OutBmp 操作即可。
3.3 为什么必须走 TBitmap32,而不是普通 TImage
用TImage加载两张 BMP 再叠加,只能靠 Canvas 的透明色或 OnPaint 手动处理;TBitmap32在内存里就是 32 位 ARGB,Pixels的读写不经过 GDI,后续滤镜和变换都能在这个缓冲上直接算。TBitmap32.Draw在把源位图画到目标缓冲时,会按照目标缓冲当前的DrawMode和CombineMode决定是否做 Alpha 混合。因此合成后的 OutBmp 要透明叠加到场景上,需要这样写:
Image32.Bitmap.DrawMode := dmBlend; Image32.Bitmap.CombineMode := cmMerge; Image32.Bitmap.Draw(0, 0, OutBmp); Image32.Invalidate;dmBlend让绘制时启用 Alpha 通道,cmMerge表示源和目标按标准 Alpha 合成公式叠加。如果忽略这两行,Draw会把 RGB 直接覆盖上去,结果是出现黑边或整块变色,这是最常见的"透明不生效"原因。
4. 像素级操作实战:位图变换、Alpha 混合与滤镜参数调优
4.1 Pixels 与 PixelS:逐像素处理的两种写法
处理单像素用Pixels没问题,但要遍历整张 1024x1024 图时,每次都走属性 getter/setter 的开销不低。Graphics32 的PixelS可以一次性把一整行像素映射到PColor32Array上:
var Line: PColor32Array; X, Y: Integer; P: TColor32; begin for Y := 0 to Bmp.Height - 1 do begin Line := Bmp.PixelS[Y]; for X := 0 to Bmp.Width - 1 do begin P := Line[X]; Line[X] := P and $FFFFFF; // 去掉 Alpha,保留 RGB end; end; end;PixelS[Y]返回第 Y 行像素的指针,可以直接修改Line[X],省去每次Pixels[X, Y]的属性调用。上面代码把所有像素的 Alpha 清成 0,常见用途是生成遮罩。注意Line指向TBitmap32内部缓冲,不需要释放。逐行访问比逐像素调用Pixels在二维遍历里大约快 3 到 5 倍,具体取决于编译器优化选项和目标平台。
4.2 Resampler 参数与变换实现
旋转缩放是 Graphics32 最容易被低估的部分。默认TBitmap32.Draw在尺寸不一致时也能拉伸,但不指定采样器时质量很差。实际项目中,我会显式给目标位图挂 Resampler:
uses GR32_Resamplers, GR32_Transforms; var Src, Dst: TBitmap32; Linear: TLinearResampler; Affine: TAffineTransformation; begin Src := TBitmap32.Create; Dst := TBitmap32.Create; Linear := TLinearResampler.Create(nil); Affine := TAffineTransformation.Create; try Src.LoadFromFile('sprite3.bmp'); Dst.SetSize(Src.Width, Src.Height); Affine.SrcRect := FloatRect(0, 0, Src.Width, Src.Height); Affine.Rotate(0, 0, 30 * Pi / 180); Affine.Scale(1.5, 1.5); Dst.Resampler := Linear; Dst.Draw(Dst.BoundsRect, Src, Src.BoundsRect); finally Affine.Free; Linear.Free; Dst.Free; Src.Free; end; end;Affine.SrcRect定义源区域,Rotate(0, 0, Angle)以左上角为圆心旋转 30 度,Scale(1.5, 1.5)在旋转后再放大。注意变换链的顺序影响结果,后调用的变换作用在已经变换过的坐标上。Dst.Resampler := Linear让目标位图在Draw发生尺寸变化时使用线性插值。参数上,TLinearResampler对质量与速度的平衡最好;TCubicResampler边缘更锐利但慢;TKernelResampler允许自定义核函数,适合做风格化缩放。
| Resampler | 适用场景 | 特点 |
|---|---|---|
| TDraftResampler | 实时预览、拖动过程 | 最快但有锯齿,适合交互反馈 |
| TLinearResampler | 缩放、旋转常规操作 | 质量与速度均衡 |
| TCubicResampler | 最终渲染、放大图 | 边缘平滑,计算量大 |
| TKernelResampler | 需要自定义滤波核 | 可配 Cosine、Spline 等核 |
在动画或实时预览阶段用TDraftResampler,等到鼠标释放或帧静止再切到TCubicResampler,这是常见做法。全流程用高阶采样器,FPS 会从 60 掉到 20 以下。
4.3 Alpha 混合的坑:黑边与预乘
把半透明精灵画到深色背景上正常,画到浅色背景上出现黑边,这是 Graphics32 初学者几乎都会遇到的。本质是位图里的 RGB 没有做预乘 Alpha:边缘像素只有一半透明,但 RGB 保留了原始颜色。cmMerge使用非预乘 Alpha 公式,黑边来自透明区域 RGB 为 0。解决办法是不要在透明区域把 RGB 清 0,而是保留原色,或者对边缘做颜色收缩。另一个注意点是dmCustom模式配合OnPixelCombine事件时,回调收到的TColor32参数是预乘后的值,此时再乘一次 Alpha 会双重暗化。判断方法是画一条半透明渐变线,和 Photoshop 的合成结果对比,变暗就是在重复乘法。
5. 组件化应用:TImage32 事件链与实时图形技巧
5.1 用 OnMouseMove 驱动局部重绘
组件层面的应用不一定要每帧全量重绘。TImage32继承自TCustomControl,可以在 OnMouseMove 里根据鼠标位置只更新局部区域,把统一绘制逻辑收到一个过程里:
procedure TForm1.Image32MouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer); var Bmp: TBitmap32; R: TRect; begin Bmp := Image32.Bitmap; R := Rect(X - 32, Y - 32, X + 32, Y + 32); ApplyContrast(Bmp, R, Round(TrackBar1.Value)); // 只处理 64x64 区域 Image32.Invalidate; end;ApplyContrast是在 4.1 的循环基础上加了区域限制:遍历R内像素,对 RGB 做对比度公式变换。这样移动鼠标时,只有鼠标周围 64 像素范围在做像素级计算,重采样滤镜的开销被挡在门外。这个写法在较大位图上比全图处理快很多,而且和窗口重绘机制不冲突。
5.2 验证渲染效果的三个技巧
第一个技巧是像素 dump。怀疑 Alpha 合成结果不对时,直接在事件里查看Image32.Bitmap.Pixels[X, Y]的十六进制值,ARGB 中 A 在最高位,$8000FF00 就是半透明绿色,比肉眼判断准确。
第二个技巧是计时。用TStopwatch包住Draw调用,统计 100 次平均耗时;如果耗时波动大,先看是否在 OnPaint 里创建了TBitmap32。每帧创建和释放会触发频繁内存分配,通常的做法是在 FormCreate 时建好缓冲,OnPaint 只做Draw。
第三个技巧是重绘范围。Image32.Invalidate请求整个区域重绘,Image32.Repaint则立即同步重绘。拖动滑块时用Repaint会阻塞消息循环,反而掉帧。正确做法是用Invalidate加DoubleBuffered := True消除闪烁,只有需要强一致结果的截图场景才用Repaint。把滑块最大值设为 360,角度参数直接绑定 TrackBar1.Position,下次拖动时就能直观看到 Rotate 在 0/90/180/270 度上的性能差异。
本文还有配套的精品资源,点击获取