简介:TeeChart 8.06完整源代码包,面向需要在Delphi、C++ Builder、Kylix等环境中开发专业图表应用的开发者,适用于高质量数据可视化、报表生成及商业智能分析场景,尤其解决了导出PDF时中文乱码的问题。整个压缩包共2362个文件,约11.49MB,其中567个dcu预编译单元、333个pas源码、229个dpk工程文件、182个dfm窗体文件及64个cpp等,既可直接编译运行,也为二次开发提供了完整骨架。资源覆盖VCL、.NET、C++ Builder等多种构建配置,包含不同版本的项目组与工程文件,便于跨版本迁移;附带的安装说明、许可文件和TeeRecompile.exe重新编译工具,能帮助开发者快速搭建自己的图表库环境。目前已有186人学习下载。通过研读源码,可深入理解图表渲染、坐标轴、系列管理等核心机制,进而定制图表类型、扩展交互功能或优化性能;同时资源内置决策立方体相关文件,适合用于多维数据分析与商业智能应用,是一份值得收藏的完整图表库学习与二次开发素材。
1. TeeChart 8.06 完整源码:直接把图表控件从黑匣子变成可改写源码
上个月接了一个老设备监控系统维护任务,界面用 Delphi 重画,里面几十张曲线、柱状、仪表盘要从前一个系统迁移过来。若每张图都从 Canvas 手写绘图逻辑,工期直接翻倍;用封装控件倒是省事,可遇到“颜色按数值阈值动态变化”“图例放在左下角自定义字体”这类需求,封装控件就像一个黑匣子,怎么都调不到位。后来翻出本地源码库里的 TeeChart 8.06 FULL SOURCE,心里有了底:这不只是一套可拖拽的 Delphi 图表组件,而是把坐标轴计算、序列类型、Canvas 渲染、设计期编辑器全部以 Object Pascal 源码开放的完整项目。它适合要维护 Delphi VCL 旧项目、需要深度定制图表的开发者;对刚接触 Delphi 的人,也可以当商业级绘图控件的设计教科书来读。
2. 源码包构成与技术栈:安装之前先看清代码地图
2.1 完整源码包里到底有什么
TeeChart 8.06 的完整源码包,并不是只有几个 .pas 文件拼起来的散包,而是带了一套完整交付目录。我拆过的这个版本大致包含以下几大块:
- 运行时核心源码:TeeChart.pas、TeeSeries.pas、TeeCanvas.pas、TeeAxis.pas、TeeProcs.pas 等,这些是控件运行时的全部实现,编译后进入你的 EXE。
- 设计期编辑器源码:TeeEdiChart、TeeEdiSeries、TeeEdiAxis、TeeEdiTool 等,这些仅在设计期起作用,用于弹出图表编辑对话框。
- 多种 Series 实现:TeeLine、TeeBar、TeePie、TeeArea、TeeGantt、TeeDonut、TeeShape 等,每种图表类型一个单元。
- Functions 函数单元:用于数据聚合,比如平均值、移动平均、指数平滑等。
- 导出与打印单元:TeeExport、TeePDF、TeeJPEG、TeePNG、TeeBMP、TeePrinter。
- 演示工程和示例数据:位于 Examples 目录,我在上手前通常会跑一遍它的 Demo,确认自己编译出来的控件行为正常。
从这些模块可以看出,TeeChart 的架构是横向分模块的:序列类型作为独立单元放在外层,核心绘制引擎放在底层,设计期工具单独隔离。这种拆分方式是 Delphi 组件开发的标准游戏规则,也是它能被二次开发的根基。
2.2 技术栈:Object Pascal、VCL 和 GDI/GDI+ 的老三样
TeeChart 8.06 是 2000 年代前期的源码,技术栈相当固定:
- 开发语言:Object Pascal,基于 Delphi VCL 框架,不能用 C++ 直接调,但 C++Builder 也支持编译同一套源码。
- 渲染后端:Windows GDI 与 GDI+。默认走 GDI,速度快;开启 GDI+ 支持后,线条抗锯齿和透明渐变效果更好,但性能稍微下降。
- 控件形态:提供 VCL 组件和 ActiveX 两种产品形态。我们这个源码包以 VCL 为主,ActiveX 版本的源码是另外的目录,而且多数场景用不到。
- 目标平台:纯粹的 Win32 桌面端,8.06 的时代还没有跨平台 VCL 的概念。
版本兼容性这块要仔细说。8.06 官方发布时主要支持 Delphi 5、6、7 和 C++Builder 5、6。源码包附带的 dpk 工程文件也是按这几个版本做的。到了 Delphi 2007 以后,包格式变化不大,你可以打开 dpk 后让 IDE 自动迁移保存为当前版本,但源码里少数 Windows API 常数的写法可能会过不了新版本编译,需要手工改个一两处。我从 Delphi 7 迁移到 Delphi 10.3 的时候,改过 TeePen 的样式枚举值和 Chart3D 的一个投影计算,大约花了两三个小时,整体迁移成本不算高。如果你手头是新 IDE,建议准备一份老 Delphi 7 虚拟机做对照,这样定位编译错误更快。
2.3 源码目录结构与包类型:先分清“运行时”和“设计期”
看源码包,我建议按下面这张表确认目录内容:
| 目录分类 | 典型文件 | 编译后产物 | 作用 |
|---|---|---|---|
| 核心运行时 | TeeChart.pas, TeeSeries.pas | TeeChart.dcu | 控件运行的主体 |
| 设计期编辑器 | TeeEdiChart.pas | TeeDesign.dcp | 只出现在 IDE 里 |
| 序列实现 | TeeLine.pas, TeeBar.pas | 随核心打包 | 具体图表类型的绘制 |
| 导出打印 | TeeExport.pas, TeePNG.pas | 打包进运行时 | 图片导出、打印 |
| 示例工程 | Examples/Delphi7/*.dpr | 可执行 Demo | 快速验证功能 |
运行时包和设计期包在 Delphi 工程里是两种不同包:运行时包会被最终程序链接,设计期包只注册到 IDE 组件面板。如果你不需要在设计期编辑图表属性,可以完全不安装设计期包,只用代码创建 Chart。但几乎所有人都会装,因为双击 TChart 弹出的图表编辑对话框真的很好用。
2.4 从源码编译安装:让 TeeChart 出现在 IDE 组件面板
整套安装流程,我自己在 Delphi 7 和 Delphi XE 上各走了一遍,步骤如下:
第一步:打开源码包根目录,找到 Packages 子目录,里面通常有多个工程组。我习惯先把源码根目录加进 IDE 的 Library 路径,这样编译器能自动找到所有 .pas 和 .dcu。
第二步:确认要编译的包名。以 Delphi 7 为例,运行时包是 Tee70.dpk,设计期包是 TeeD7.dpk。新建工程时只要在 Uses 中引用 TeeChart 单元,编译器就会自动从运行时包中取到 dcu。
第三步:编译运行时包,再编译设计期包。如果你喜欢命令行操作,可以在源码目录执行:
dcc32 -B -I. -U. Tee70.dpk dcc32 -B -I. -U. TeeD7.dpk-B 表示强制重编译所有单元,避免旧 dcu 干扰;-I 指定 Include 搜索路径;-U 指定单元搜索路径。这里的.指的是当前目录,你需要先 cd 到 Packages 目录再执行。若是新版本 IDE,则需要在 IDE 中打开 dpk,另存为当前版本工程,再执行 Build。
第四步:打开 IDE 的 Component / Install Packages,点击 Add,选中编译好的 TeeD7.bpl。此时设计期包被安装,组件面板会新增 TeeChart 组件页。
第五步:新建一个 VCL 工程,在组件面板拖一个 TChart 到窗体上,双击打开图表编辑器。如果编辑器能正常弹出来,说明运行时包和设计期包都装好了。
安装过程看起来简单,但这里翻车的概率很高。最常见的是忘掉 Library Path,导致编译设计期包时提示 Cannot find unit TeeCanvas。别急,它本质是路径搜索不到单元,加好路径再 Build All 就能解决。
3. 核心模块拆解:从数据到像素的完整执行链路
3.1 Series 类层级:一张图的所有可能,都藏在这棵继承树里
TeeChart 的组件编排,核心是 TChart 控件,而真正决定图表长什么样的,是挂在 TChart 下的 SeriesList。一个 Chart 可以挂多个 Series 作为不同的数据层,比如一层柱状图、一层折线图叠加显示。Series 的基类是 TCustomSeries,它定义了数据存取、坐标换算、绘制调用的公共协议。
我用源码里的 TLineSeries 来演示 Series 类的骨架:
type TLineSeries = class(TSeries) private FLinePen: TChartPen; FPointer: TChartPointer; FStairs: Boolean; protected procedure DrawSeries(ACanvas: TCanvas3D; const Rect: TRect); override; procedure DrawLegend(ACanvas: TCanvas3D; const Rect: TRect); override; public constructor Create(AOwner: TComponent); override; destructor Destroy; override; property LinePen: TChartPen read FLinePen write SetLinePen; property Stairs: Boolean read FStairs write SetStairs; end;FLinePen 用于控制线条颜色、粗细、线型;FPointer 用于画数据点标记,可以显示为圆、方块、三角等。Stairs 打开后,折线会变成阶梯状,用于表示采样时刻间的保持值。这两个属性最终都会在绘制时影响画线的 GDI Pen 创建参数。
所有动态数据都存放在父类 TSeries 的 ValueList 和 LabelList 中,内部是两个用 TChartValueList 管理的浮点值数组。DrawSeries 方法就是读取这些数组,调用 Canvas 以 LineTo 串起每个点。源码里几乎每种 Series 都重写了 DrawSeries,所以你要理解一种新的图表类型,主线就是看它的 DrawSeries。
3.2 坐标轴的计算:屏幕上的横坐标是怎么从数据值换算来的
图上反正都要画坐标轴,而坐标轴计算是 TeeChart 最值得读的一段代码。以水平轴为例,TChartAxis 内部持有 Maximum、Minimum 两个数据范围,以及 CalcRect 方法负责把数据值映射成像素坐标。
function TAxis.CalcRect(const Value: Double): Integer; var Factor: Double; begin if Horizontal then begin Factor := (IEndPos - IStartPos) / (Maximum - Minimum); Result := IStartPos + Round((Value - Minimum) * Factor); end else begin Factor := (IEndPos - IStartPos) / (Maximum - Minimum); Result := IEndPos - Round((Value - Minimum) * Factor); end; end;这里 IStartPos 和 IEndPos 是轴在绘图客户区中的起始和结束像素位置。水平轴的 IStartPos 通常在左边,垂直轴则是从上到下。Factor 就是“单位数据值等于多少个像素”的比例因子。当 Maximum 等于 Minimum 时会出现除零,源码的 SetMaxMin 方法会先做保护,使两值之差至少为 1,避免这种边界条件。
调试坐标相关的 bug,我最常做的是在 CalcRect 里临时输出 Value、Minimum、Factor,确认是不是轴范围没有被数据撑开。很多时候图是“空白”的,不是没数据,而是轴的 Maximum 被手动改成 0 了,导致数据全部跑到可见范围之外。把 Automatic 设为 True 就能让轴重新按数据自动计算。
3.3 Canvas 渲染机制:GDI 与 GDI+ 切换的分水岭
TeeChart 源码中,所有绘图操作都限定在 TCanvas3D 接口内,TCanvas3D 同时承担 2D 和 3D 的坐标变换。TCanvas3D 内部有个抽象绘图引擎接口,实现了 GDI 和 GDI+ 两套后端,根据属性设置启用不同的实现。
这是 GDI 后端中画线的一段典型代码:
procedure TGDICanvas.LineTo(const X, Y: Integer); begin if FUseGDI then begin Windows.LineTo(FDC, X, Y); FCurrentPos := Point(X, Y); end else begin FGPlusCanvas.DrawLine(FGPPen, FCurrentPos.X, FCurrentPos.Y, X, Y); FCurrentPos := Point(X, Y); end; end;FDC 是设备上下文句柄,这里直接调 Windows API LineTo,性能最好。在 GDI+ 分支中,则要把起点终点都传给 DrawLine。GDI+ 的线帽、抗锯齿和透明度都更丰富,但每次画线都要构造 GDI+ 对象,所以开销更大。我一般在最终交付版本中默认关闭 GDI+,只在需要专业视觉效果时开启。
8.06 源码里还有一个 TChartCanvas 的中间层,它介于轴和 TCanvas3D 之间,主要处理 Legend 和 Chart 背景的绘制。这一层的存在让轴坐标的计算与 Canvas 的实际后端解除耦合。理解了这个分层,你要替换底层渲染后端就不至于改成一团浆糊,只需要实现一套新的 Canvas3D 子类。
3.4 数据绑定与 DBChart:数据库字段如何直接变成图表序列
8.06 的 TDBChart 是很多 MIS 系统里用到的控件。它的实现方式是给 Series 增加 DataSource 相关属性,内部实现了与 TDataSet 字段的联动。
procedure TDBChartSeries.AddRecord(ARecordIndex: Integer); begin if DataSource is TDataSet then begin ValueList.Add(TDataSet(DataSource).FieldByName(YField).AsFloat); if XLabelsField <> '' then LabelList.Add(TDataSet(DataSource).FieldByName(XLabelsField).AsString); end; end;ARecordIndex 参数是当前记录在数据集中的游标号,实际使用时它并不直接用于定位,因为 TDataSet 可能处于过滤或非顺序状态。源码仍然保留这个参数,是为了兼容老版本的数据集接口。字段的值类型通过 AsFloat 转换,字符串标签用 AsString。处理大数据集时,这种逐条访问字段的方式效率较低,大约到两三万条数据时就会卡顿。我一般会先把 TDataSet 查询结果放进内存 ClientDataSet,再一次性赋给 Series 的 XValues 和 YValues,绕开 TDBChart 的逐条取值逻辑。
3.5 图表编辑器:设计期界面是怎么反过来驱动属性修改的
TeeChart 的图表编辑器不是一个简单属性网格,而是按 TabPage 分区的大对话框。TeeEdiChart 里有一个主窗体,上面放置 TPageControl,每个 Tab 对应一类属性:General、Series、Axis、Legend、Tools。这些 Tab 页面由多个单元文件提供,比如 TeeEdiSeries 提供 Series 页,TeeEdiAxis 提供轴页面。
双击图表时,TChart 控件调用 ShowEditor 方法,创建编辑器窗体并传入 Self 指针。编辑器修改属性时直接对 Self 的属性赋值,并调用 Repaint 刷新显示。这种“一个窗体直接操作控件实例”的模式在 Delphi 组件设计中很常见,优点是调用链短,缺点是编辑器与控件强耦合,所以要把编辑器放到独立的设计期包里,避免它进入运行时 EXE。我在自定义 Series 时,也会模仿这种模式,给我的新 Series 单独建一个编辑页,把自定义属性都丢到对应 TabPage。
4. 避坑与常见排查:编译、安装、绘图中我踩过的几个坑
4.1 现象:安装后组件面板找不到 TeeChart 控件
现象:按上一章的步骤编译全部成功,bpl 也生成了,但新工程里面找不到任何 TeeChart 相关控件。
原因:一是没有安装设计期包,只编译了运行时包;二是 IDE 里原本装了其他版本的 TeeChart,两个包都注册同一个类名 TChart,导致后安装的包被忽略;三是注册类型使用的是带版本前缀的名称,导致 IDE 面板没有刷新。
解决:先打开 Component / Install Packages 对话框,确认设计期包在列并处于勾选状态。若找不到,用 Add 方式添加 bpl。若存在重复包,把旧包移除。如果怀疑注册名冲突,在源码的 Register 过程里加一行 ShowMessage 输出已注册的类名,重新 build 后看 IDE 报不报名字冲突。我最终是靠卸载旧版本解决的,从那以后,安装任何组件都先把旧组件包清干净,不让这种问题浪费半天时间。
4.2 现象:编译报错 Cannot find unit TeeCanvas
现象:用新 Delphi 编译源码时,报“Cannot find unit TeeCanvas”或者“File not found: TeeCanvas.dcu”。
原因:多数情况下是 IDE 的 Library 搜索路径没包含源码根目录。编译器从每个包文件的目录开始找单元,找不到就按 Library 路径找,最后去 IDE 自己的默认路径找。源码包若放在一个深层目录,很容易因为路径配置不全而找不到较冷门的单元。
解决:把源码根目录、Source 目录、Editor 目录分别加入 Library 搜索路径,路径优先级要高于其他第三方包。更保险的做法是在 dpk 工程文件的搜索路径里写相对路径,如..\..\Source。改完路径后必须重新 Build All,不要只 Build 当前工程,否则被引用的旧 dcu 仍会残留。遇到个别新版 IDE 因 Windows API 改名导致编译失败,那是另一个问题,按报错的行号逐个改 API 名即可。
4.3 现象:运行后图表区域空白,不画任何数据
现象:程序运行正常,窗口也在,但 Chart 区域内一个像素都没有绘制。给 Series 添加了数据仍无效果。
原因:检查 Series 的数据点数;常见的是轴范围不是自动。另一个常见原因是 Chart 的背景画法设置成了无笔刷且无边框,导致整个区域绘制后是透明的,看起来像是空白的。还有冷门原因:控件尺寸在 OnCreate 时还没确定,此时添加数据后自动重绘,但客户区宽度为 0,CalcRect 算出的所有坐标都是 0。
解决:先看 Series.Count 是否大于 0,再看 LeftAxis.Automatic 和 BottomAxis.Automatic 是否为 True。然后检查 Chart.Color 是否与窗体颜色一致。最后记得在 Form.OnShow 后再添加数据,或者调用 Chart1.Invalidate 强制重绘。通常这一步一操作,空白问题就能看到数据了。
4.4 现象:导出 PNG 或 JPG 时颜色偏色,甚至出现黑色背景
现象:在界面上显示正常的图表,用 TeeExport 单元导出为 PNG,再打开图片发现背景发黑或颜色整体加深。
原因:导出代码里默认使用了 GDI+ 渲染,而窗口上用的是 GDI 渲染。GDI+ 的位图格式初始化与 GDI 设备上下文不同,且 PNG 保存时透明通道被当作黑色出现。8.06 的 GDI+ 设置是全局的,导出接口有时会自行修改全局状态,没有恢复原状。
解决:导出图片前,用代码临时把 TeeChart 的 UseGDIPlus 属性设为和窗口一致,导出后再恢复;或者直接导出 BMP,BMP 的格式最保守,不会出现透明通道问题。若是 JPG,注意把 JPEG 压缩质量设为 90 以上,画面不会有明显失真。下面是一段稳定的导出写法:
procedure ExportChartToPNG(AChart: TChart; const AFileName: string); var Tmp: TBitmap; begin Tmp := TBitmap.Create; try Tmp.Width := AChart.Width; Tmp.Height := AChart.Height; AChart.Draw(Tmp.Canvas, Rect(0, 0, Tmp.Width, Tmp.Height)); Tmp.SaveToFile(AFileName); finally Tmp.Free; end; end;该方法先把图表画在一个临时位图上,再从位图保存为 PNG。因为位图的绘制上下文与界面 Canvas 一致,不会出现颜色偏移。这段代码是我自己的常用工具函数,放在公用单元里随时调用。
4.5 现象:坐标轴缩放后,线条出现抖动或像素级偏移
现象:开启鼠标缩放后,放大曲线到一个较大的倍数,某几条折线以锯齿状轻微抖动,坐标数值和显示位置对不上。
原因:CalcRect 里的 Round 操作在缩放比例达到每像素零点几分的时候,会产生约 1 像素的舍入误差。多段折线叠加时,这个误差会在相邻点之间产生视觉抖动。源码没有像现代图形库那样引入亚像素精度。
解决:临时修改源码,在轴内部为水平轴和垂直轴分别保存小数坐标,映射函数返回 Double 而不是 Integer,在 DrawSeries 画线时用 Float 坐标传入 GDI+。用 GDI+ 版的 LineTo 接受 Double 参数,抖动基本消失。这个改动会涉及 TChartCanvas 的若干方法签名,风险稍高,但值得为高倍缩放场景做。若不想改源码,可以退一步,在 MouseWheel 缩放事件里把轴的 Maximum 与 Minimum 做一次圆整,让比例因子保持整数倍,视觉抖动也可以减轻。
5. 二次开发思路:自定义一个上下界区域填充序列
5.1 确定继承方向
先想定制目标:要一个上下界区域填充图,可以应用到任何数据上。可能的继承路线有两个:从 TAreaSeries 继承,它已经有从基线到曲线的填充逻辑,但它的基线是零或某个固定值,改起来更像打补丁;从 TCustomSeries 继承,自己管理数据列表和绘制,自由度更高,也更符合 TeeChart 的官方扩展方式。我选后者。
5.2 核心实现:点数列表和 DrawSeries 覆写
自定义序列的基本骨架如下:
type TRangeSeries = class(TCustomSeries) private FUpper, FLower: TChartValueList; protected procedure DrawSeries(ACanvas: TCanvas3D; const Rect: TRect); override; function GetCount: Integer; override; public procedure AddRange(AX, ALower, AUpper: Double); end;实现时,构造器创建上下界两个值列表,DrawSeries 把下界点的像素坐标从左到右收集到数组 Pts,再把上界点从右到左追加上去,构成闭合多边形后调用 ACanvas.Polygon 填充:
procedure TRangeSeries.DrawSeries(ACanvas: TCanvas3D; const Rect: TRect); var Pts: array of TPoint; I: Integer; begin SetLength(Pts, GetCount * 2); for I := 0 to GetCount - 1 do begin Pts[I] := CalcPosValue(I, FLower.Value[I], Y); Pts[GetCount * 2 - 1 - I] := CalcPosValue(I, FUpper.Value[I], Y); end; ACanvas.Pen.Style := psClear; ACanvas.Brush.Color := clHighlight; ACanvas.Polygon(Pts); end;CalcPosValue 是 TCustomSeries 提供的工具,把数据索引和数值一并转换成画布坐标,已经处理了轴映射和 3D 偏移,不需要你再调用轴计算。这段代码的关键是点序:先下界正向收集,再上界反向收集,闭合起来的区域才不会出现线条交叉。Brush 用半透明色可做叠加效果。
5.3 注册新序列并暴露到图表编辑器
要让新序列能出现在组件面板,单元底部写 Register 过程并把类注册为组件;要让图表编辑器也能新建它,需要调用 RegisterSeriesClass 把它加入内置的序列类型列表。两个注册缺一不可,否则只能在代码里手动 new。
procedure Register; begin RegisterComponents('TeeChart User', [TRangeSeries]); RegisterSeriesClass(TRangeSeries, 'Range Series', 'User Defined'); end;注册完重新编译设计期包后,双击任意 TChart 打开编辑器,新增 Series 对话框里就能找到 Range Series。这一步我一开始总忘记,后来强制把这两行当模板使用,每次新增序列都一起带上。
5.4 验证方法与一个持久化技巧
验证时用 100 个随机点测试填充是否完整,再把上界设成小于下界,确认它不会悄悄给你交换数据;最后用临时 Bitmap 导出 PNG,避免颜色或透明通道出错。调试完如果要把序列存进 dfm,记得把 FUpper 和 FLower 属性声明为 Published,否则重新打开工程会丢数据。TeeChart 自带的值列表控件已经实现了读写方法,你只要暴露发布属性即可。
我从第一次接触 TeeChart 源码到现在,养成的习惯是:凡是新增序列,必按“继承基类—覆写 DrawSeries—注册两处—持久化验证”这个顺序走一遍。看起来多出两小步,但后面维护和升级都能少踩很多坑。希望这篇拆解能帮到你。
本文还有配套的精品资源,点击获取