简介TEECHART 8.06 FULL SOURCE 面向需要高质量图表与深度定制能力的 Delphi、CBuilder 及 Kylix 开发者提供完整图表库源代码可用于数据分析、可视化报告与商业智能等场景。压缩包共 2362 个文件约 11.49MB以 333 个 pas 源码、567 个 dcu 编译单元、229 个 dpk 包工程、182 个 dfm 窗体及 bmp/res 资源为主并含安装许可与版本说明文档便于理解工程组织与集成方式。已有 186 人学习下载。其核心价值在于可研读并改造绘图、导出与决策立方体等模块尤其修复了 PDF 导出中文显示问题适合需要跨平台图表控件、中文报表输出或二次开发的中高级开发者参考。1. 老工控项目里那张画不动的曲线图把我推回了 8.06 的 full source接手某公司一套运行了十来年的上位机程序界面是 Delphi 7 的 VCL 工程要求新增一页实时曲线加一页 K 线。项目里早装了某个图表控件但只有一个打分发出去的 dcu 和一堆 bpl双击进去改不了任何东西坐标轴想加个自定义刻度都得求原厂。翻资料时翻出了 TEECHART 8.06 FULL SOURCE 这份包几百个 .pas 单元、几套 .dpk 包、一堆 Demo 工程从画布抽象层到每条 Series 的像素计算全在里面。它适合两类人一类是被遗留 Delphi / CBuilder 工程绑住、必须在地面端把图表做深做透的开发者另一类是单纯想看看一个成熟绘图控件库怎么把坐标系、Canvas 后端、序列化拆开的人。不适合想找开箱即用前端图表库的人这份源码是 Object Pascal 写的跑在 VCL 里跟浏览器没关系。2. 源码包结构拆解8.06 的 full source 到底给了什么2.1 技术栈定位Object Pascal VCL Canvas 的老派绘图体系拆之前先把技术栈钉死。这套东西的运行时是 Delphi 的 RTL 和 VCL语言是 Object Pascal绘制最终落到 Windows 的 GDIGraphics32 时代之前的那套 MoveTo/LineTo/Rectangle 原语上。所以它没有 CSS没有硬件加速3D 部分有自己的软件投影管线部分版本另挂了 OpenGL 后端所有图形都由 Canvas 一层层算出来。控件本身是“组件 设计期编辑器”的组合TChart 是一个 TCustomPanel 派生的可视组件挂在 Form 上就能用配套的 TeeEdit / TeeGallery 提供 IDE 里的图表类型选择框、序列编辑器、导出对话框。这些编辑器也全是源码意味着你可以把那个丑了十年的默认编辑框整个换掉。多版本兼容靠条件编译。包里有一个集中的符号定义单元TeeDefs 一系的 .inc 和 .pas用{$IFDEF VER140}、{$IFDEF VER150}这种按编译器版本号分支的写法把 Delphi 5 一直到 2009、以及 CBuilder 各版本的差异吃掉。这是那个年代 Delphi 生态的标准做法也是后面编译翻车的第一大来源。值得一提的是包里有大量{$IFNDEF TEESTANDARD}、{$IFDEF TEEPRO}之类的开关。商业控件的源码包通常分 Standard / Pro 两档full source 给的是 Pro 全量但源码里仍然保留了降级开关删掉某几行就能裁出一个精简版。二次开发时这套开关非常好用你要砍功能减体积照着这些符号关就行不用自己去注释成百上千行。2.2 单元分层从 TeeDefs 到 Chart 再到 Series不要一上来就打开最大的那个单元会晕。按职责把它切成五层读起来会顺很多。不同小版本之间单元拆分略有出入以包里实际的 .pas 为准但分层逻辑是稳定的。层代表单元职责基础定义TeeDefs、TeeProcs编译开关、颜色常量、通用工具函数、异常类画布层TeCanvas 一系TTeeCanvas 抽象类及其 GDI / GDI / OpenGL / 打印派生图表骨架ChartTCustomChart、TChart管标题、图例、坐标轴、工具、重绘调度数据序列Series 一系TChartSeries 基类、TChartValueList 值列表、几十种具体序列周边设施TeeStore、TeeExpor、TeeEdit、TeeTools模板序列化、图片/PDF 导出、编辑器、光标与标注工具理解这张表的顺序很关键Series 不自己画到窗口上它只拿到一个 ParentChart 给的 Canvas然后往里灌原语坐标轴也不自己算像素它问 Chart 要一个“值 → 屏幕坐标”的映射函数。整条链是单向的Chart 是唯一知道“现在这块画布多大”的角色。这个设计在 8.06 里贯彻得很彻底也是它能在 GDI、打印、位图导出、OpenGL 之间换来换去的根本原因。读源码时我建议从 Series 一系的最小实现入手——找一个线条序列看它怎么把一条 ValueList 变成折线比看 Chart 里两千行的重绘调度要轻松得多。看完再回头看 Chart 的调度会豁然开朗。2.3 编译产物dpk / bpl / dcp / lib 各自是什么full source 交到手上不是一堆散文件它带了完整的包工程。Delphi 侧是.dpkCBuilder 侧是.bpk编译出来是.bpl运行时动态库加.dcp编译期符号信息C 侧还要额外的.lib和.hpp头文件。一般拆成两个包一个是运行时包纯逻辑不含编辑器你的 exe 依赖它另一个是设计期包注册组件到 IDE 面板依赖运行时包。这么分的好处是你发布程序时可以不带上编辑器那坨东西体积能小一截。自己改源码时也要保持这个分法别图省事把编辑器代码编进运行时包最后发布出来一堆用不上的窗体资源。命令行编译一个运行时包大致长这样rem 切到包的工程目录指定源码搜索路径和输出目录 cd /d D:\tee806\Packages\Delphi7 rem -B 强制全量重编译-U 加单元搜索路径-E 指定 bpl 输出位置 dcc32 -B -U..\..\Sources -E..\..\Lib TeeChart8.dpk-B是必须的源码包里的 .dcu 往往是别的 Delphi 版本编出来的增量编译会拿旧符号去链接报一堆莫名其妙的错。-U指向 Sources 目录别指向子目录包里的单元是平铺的。-E把产物丢到独立目录避免和你项目里已有的同名 bpl 打架——这是血泪经验同名 bpl 在系统 PATH 里被先加载你改的代码压根没生效调试半天以为源码没编进去。3. 核心绘制链路一次重绘从 TChart.Paint 走到 Series.Draw3.1 Canvas 抽象层所有花哨图形最后都落到几个原语上图表控件看着复杂剥到最里面就是“把值映射成坐标然后调画布画线画块画字”。8.06 把这件事抽成了 TTeeCanvas一个纯虚基类声明 MoveTo、LineTo、Rectangle、Ellipse、Polygon、TextOut 这些方法具体实现分给 GDI 版、GDI 版、打印版和 3D 版。好处很直接Series 的绘制代码只认 TTeeCanvas不认 Windows HDC。你在 PC 上画的折线换一个 Canvas 实例就能原样画到打印机、位图或者 PDF 上。这是它导出功能不用重写一遍绘制逻辑的原因。下面这段是简化后的画布接口形态不是逐字照抄源码但结构一致看个意思type TTeeCanvas class(TPersistent) public // 画笔与画刷状态所有派生类共用 Pen: TChartPen; Brush: TChartBrush; Font: TFont; // 平面原语派生类各自实现 procedure MoveTo(X, Y: Integer); virtual; abstract; procedure LineTo(X, Y: Integer); virtual; abstract; procedure Rectangle(X1, Y1, X2, Y2: Integer); virtual; procedure Ellipse(X1, Y1, X2, Y2: Integer); virtual; procedure Polygon(const Points: array of TPoint); virtual; abstract; procedure TextOut(X, Y: Integer; const St: String); virtual; abstract; // 三维投影把逻辑三维点折算成二维屏幕点 function Calculate3DPosition(X, Y, Z: Integer): TPoint; virtual; end;Pen和Brush用的是控件自己的 TChartPen / TChartBrush不是 VCL 原生的因为要支持透明、渐变、圆角端点这些 VCL 老画笔没有的属性同时也为了让打印机能识别。Calculate3DPosition是 3D 图表的关键钩子折线、柱体、曲面都靠它把 Z 轴折进屏幕坐标二维模式下它就是个恒等映射。你如果要做等轴测投影或者自定义视角改这一个函数比改几十处绘制代码省事得多。派生类里 3D 版最值得看它重写了 Polygon 和 TextOut一个负责给柱体做深度排序一个负责让标签沿着轴倾斜摆放。很多“3D 图看着假”的问题根子就在这里深度排序用的是面中心的 Z 值两个面中心一样深时顺序就飘。3.2 TCustomChart 的绘制调度谁先画、谁后画Chart 的重绘入口是 Paint但真正干活的是内部一个 Draw 方法。它按固定顺序调度各个部件顺序不能乱因为后画的内容要盖住先画的。简化后的调用链是这样procedure TCustomChart.InternalDraw(Canvas: TTeeCanvas; IsMetafile: Boolean); begin // 1. 背景渐变、图片、墙、地板 DrawBackground(Canvas, IsMetafile); // 2. 标题与副标题 DrawTitle(Canvas); DrawSubTitle(Canvas); // 3. 坐标轴与网格线必须先于序列否则网格盖住数据 DrawAxes(Canvas); // 4. 数据序列按 ZOrder 从大到小画保证层级正确 DrawSeries(Canvas); // 5. 图例与工具光标线、标注框要在最上层 DrawLegend(Canvas); DrawTools(Canvas); end;第 3 步和第 4 步的顺序是新手最容易踩的坑把网格画在序列后面折线就被网格线切成一段段。要改这个行为不要动这里的顺序去看每个部件的Transparent和ZOrder属性控件已经给了开关。第 4 步里有个细节值得学Series 是按 ZOrder 排序后逐个画的而不是按加入顺序。这么做是为了让用户能在编辑器里拖动调整图层。代价是每帧都要排一次序序列特别多的时候比如二十条以上会有可测的开销。我一般会在批量加序列时先把 ZOrder 一次性设好避免运行期反复重排。IsMetafile这个参数贯穿始终它告诉每个部件“你现在画的是图元文件不支持位图操作”。导出 EMF/WMF 时很多效果会降级比如渐变变成纯色、透明度丢失。这不是 bug是设计取舍改的时候别硬怼。3.3 Series.Draw 与 ValueList数据是怎么变成像素的所有序列的共同祖先是 TChartSeries它持有若干个 TChartValueList——一个 X 列表加上一个或多个 Y 列表气泡图还要 SizeListK 线要 Open/High/Low/Close 四个列表。ValueList 本质是一个带统计能力的动态数组能算最大最小值、平均值、总和因为坐标轴自动缩放要频繁问这几个数。把值换算成屏幕像素的核心就两个函数简化后function TChartSeries.CalcXPos(ValueIndex: Integer): Integer; begin // 横轴位置先取 X 值再交给 Chart 的横向映射 Result : ParentChart.XScreenValue(ValueList[ValueIndex].Value); end; function TChartSeries.CalcYPos(ValueIndex: Integer): Integer; begin // 纵轴位置注意屏幕 Y 轴向下而数据 Y 轴向上转换里做了翻转 Result : ParentChart.YScreenValue(ValueList[ValueIndex].Value); end;ValueIndex是数据点下标不是 X 值本身这点务必分清横轴如果是分类模式Label 列表X 值就是 0、1、2 这样的序号真实标签存在另一个列表里。XScreenValue/YScreenValue属于 Chart内部会先减掉轴最小值、再除以轴范围、再乘上像素长度最后加上绘图区偏移。轴的反向、对数、自定义刻度全部在这一层被吃掉所以 Series 完全不用关心轴长什么样。明白了这层你要给某个序列做“值超过阈值就画红点”的效果正确做法是在它的 Draw 里判断而不是去改 Chart 的映射函数——后者会污染所有序列。4. 编译与集成把 8.06 源码接进现有工程4.1 环境准备与源码路径第一步不是打开 IDE是先把目录整理干净。源码包里通常混着 Sources、Packages、Examples、Demos 几大块还有各 Delphi 版本的子目录。我的习惯是新建一个工作根目录把 Sources 单独拷一份出来作为“我改过的版本”原始包原封不动放着当参照后面 diff 的时候你会感谢自己。然后确认两件事你用的 Delphi 版本对应的条件编译符号是否在定义单元里被覆盖到Sources 目录下有没有残留的 .dcu。残留的 .dcu 是最阴的坑因为编译器优先用 .dcu 而不是 .pas你明明改了源码编出来行为一点没变。清理脚本我一般写成这样rem 删掉源码目录里所有编译中间产物只留源码 del /s /q D:\tee806\Sources\*.dcu del /s /q D:\tee806\Sources\*.dcp del /s /q D:\tee806\Sources\*.local顺带把.local也删了那是 IDE 记录的单元搜索路径缓存指向别人的机器会干扰你。IDE 里要做的是把 Sources 加入 Library Path注意是全局的 Library Path 不是项目级的 Search Path。因为设计期包是 IDE 加载的它读全局路径。项目级路径只影响你的 exe 编译两边都配一遍最稳。顺序上把源码路径放到最前面避免系统里装的旧版控件抢先。4.2 用 dpk 打包安装别用“Add to Project”有人图省事直接把几百个 .pas 拖进项目里编译。小 Demo 能跑正式项目会死得很难看每个用到图表的单元都编一份符号exe 体积翻倍还容易出现两份 RTTI 导致编辑器识别不了组件。正确流程是编包。打开对应版本目录下的运行时包工程Build再打开设计期包Install。装完在组件面板上应该能看到图表那一页。如果 Build 报Unit X was compiled with a different version of Y八成是路径里混进了旧 dcu回到 4.1 清一遍。CBuilder 用户多一步.bpk编出.bpl和.lib之后还要把.hpp头文件和.lib加到工程的 Include / Library 路径里否则 C 侧链接会找不到符号。头文件是包编译时自动生成的别手工抄。4.3 最小可跑示例一张实时曲线装好之后写个最小验证确认源码真的生效了。放一个 TChart 和一个 TTimer代码如下procedure TForm1.FormCreate(Sender: TObject); begin // 关掉 3D 和自适应视野实时曲线用固定窗口更好控制 Chart1.View3D : False; Chart1.Legend.Visible : False; // 建一条折线序列并挂到图表上Owner 传 Chart1 让它自动释放 Series1 : TLineSeries.Create(Chart1); Series1.ParentChart : Chart1; Series1.Title : channel-A; // 横轴改成分类模式X 值用序号避免时间戳换算 Chart1.BottomAxis.LabelsMultiLine : False; Chart1.BottomAxis.Automatic : True; end; procedure TForm1.Timer1Timer(Sender: TObject); begin // 每帧只追加一个点滚动窗口交给 Chart 处理 Series1.Add(Sin(Now * 1440) * 50 Random(10)); // 超过 200 点就丢掉最老的防止 ValueList 无限增长 if Series1.Count 200 then Series1.Delete(0); end;Series1.Add只给一个值时横轴用内部序号给两个值时第一个是 X。Delete(0)是 O(n) 的数组搬移200 点无所谓几万点频繁删头会卡那种场景应该换成环形缓冲配合SetValues或者直接用TeeSeriesBand之外的滚动模式。跑起来如果曲线在动说明包引用的是你编的源码。想验证得更彻底在 Series 的绘制函数里插一句OutputDebugString改完编一次看调试器里有没有输出这是确认“我改的代码真的在跑”的最快方式。5. 避坑与排查条件编译、闪烁、卡顿、导出这四类最容易翻车5.1 现象编译报“Unit TeeDefs was compiled with a different version”或者一装包 IDE 就崩原因基本是两个。一是源码目录里残留了别的 Delphi 版本编出的 .dcu编译器优先拿它二是设计期包和运行时包没编成一对IDE 加载了新的设计期包却还引用着系统里旧版运行时 bpl。Delphi 的包管理是“版本必须严格对齐”一个字节都不能错。解决按顺序来先清所有 .dcu/.dcp/.local再确认 IDE 的 Library Path 里没有旧版控件目录然后先编运行时包、后编设计期包中间不要切换平台。装完在 IDE 的 Install Packages 里检查一下把重复的图表包卸掉只留一个。我吃过一次亏两套包同时装着编译期不报错运行期随机 AV查了一整天才发现是重复注册的类。5.2 现象窗口拖动或缩放时图表闪成一片白某些区域还花屏原因是控件的 Paint 里直接往窗口 DC 上画没有经过双缓冲系统在重绘的间隙把窗口背景刷了出来。老 VCL 控件都这毛病跟源码无关但源码给了你改的机会。解决有三层。最轻的是把 Chart 的DoubleBuffered打开让它走 VCL 的内存位图中一层是在你的 Form 上包一层WM_ERASEBKGND直接返回 1屏蔽背景擦除最狠也最有效的是自己派生一个 Canvas先把整张图画进内存位图再一次性 BitBlt 上去。第三种做法在数据量大时也要小心位图尺寸跟着窗口走最大化到 4K 屏会吃掉几十 MB 内存。顺带提一个玄学问题闪烁有时候不是 Chart 的锅是父面板的ParentBackground和主题服务在打架。把父容器设成不透明、去掉主题勾选往往一改就好。5.3 现象数据点涨到几万之后鼠标一动就卡CPU 单核打满先看是不是每次 Timer 都在触发整图重绘。很多人写实时曲线习惯每个数据点调一次Repaint一秒几十次全量重绘什么控件都顶不住。改成固定帧率刷新比如 2030 FPS 一个定时器统一刷卡顿立刻缓解。再看数据结构。Delete(0)每次都要搬移整个数组数据量大时是平方级开销换成固定长度环形缓冲或者干脆保留全部数据、只让坐标轴可视范围跟着滚绘制时按可视范围内插采样。8.06 的序列有DrawAllValues之类的开关关掉之后它会按像素宽度抽样画点视觉上看不出差别性能差一个数量级。最后看抗锯齿和渐变。GDI 后端画几千条线的抗锯齿非常贵实时曲线阶段建议关掉等用户暂停了再开。5.4 现象导出 PNG 是空白导出 PDF 字体全变成方块空白多半是导出时图表还没完成一次布局绘图区尺寸为 0。导出前手动调一次Chart1.Paint或者设置好明确的宽高别指望它自己算。另外有些后端不支持离屏绘制必须给一个真实的 Canvas 句柄。字体变方块是编码问题不是字体缺失。老版本导出走的是 ANSI 代码页中文字符在转换时被替换掉了。解决是导出前把字体统一成 TrueType 且明确 CharSet或者改用支持 Unicode 的后端。这个问题在只跑英文的测试机上永远复现不了。还有一个隐坑导出用的 Canvas 和屏幕用的不是同一个实例你在屏幕上设的 Pen 宽度是像素单位到 PDF 里变成了点1/72 英寸导出的线会明显变细。做正式报表输出时缩放系数要单独算一遍。5.5 现象换了高分屏或换了系统字体标签全部错位、图例被截断8.06 那个年代的控件默认按 96 DPI 硬编码了一堆像素常量比如图例的内边距、刻度标签的间距。系统缩放 150% 之后字体变大了但常量没变标签就会压到轴上。解决是在 Form 创建时按Screen.PixelsPerInch / 96算一个比例把 Chart 的字体、边距、刻度长度统一乘一遍。中文还有额外一层默认字体在中文环境下字宽和英文差很多图例按字符数算的宽度会不够。稳妥做法是让图例宽度自适应给一个最大宽度让它自己撑或者干脆把图例挪到图表下方横排。6. 二次开发派生一个自定义序列顺带把画布后端换掉源码在手最值钱的不是改配色是派生自己的 Series。举个实际需求设备状态要画成一条随时间变化的状态带不同状态不同颜色中间还要标出报警点。现成序列没有一个能直接干这活但派生一个只要百来行。type TStateBandSeries class(TChartSeries) private FStates: array of Integer; // 每个采样点的状态码 protected // 核心把状态码画成彩色矩形块横跨相邻两点的 X 区间 procedure DrawValue(ValueIndex: Integer); override; public constructor Create(AOwner: TComponent); override; procedure AddState(AState: Integer; const AX: TDateTime); end; procedure TStateBandSeries.DrawValue(ValueIndex: Integer); var X1, X2, Y1, Y2: Integer; begin if ValueIndex 0 then Exit; // 第一个点没有左邻跳过 X1 : CalcXPos(ValueIndex - 1); X2 : CalcXPos(ValueIndex); Y1 : ParentChart.ChartRect.Top; Y2 : ParentChart.ChartRect.Bottom; // 按状态码换颜色画布从父图表拿保证导出时一致 ParentChart.Canvas.Brush.Color : StateColor(FStates[ValueIndex]); ParentChart.Canvas.Rectangle(X1, Y1, X2, Y2); end;关键在DrawValue用的是ParentChart.Canvas而不是自己 new 一个画布这样屏幕、打印、图片导出三条路径自动复用同一份绘制代码。CalcXPos是父类给的映射函数你只管用轴怎么缩放不用管。构造里记得把FStates的长度跟 ValueList 对齐AddState里同时往 ValueList 和 FStates 追加保持索引一致——这是派生序列最容易出错的地方两个数组长度一旦错位画出来的色块就整体偏移一格。派生完不要直接往 IDE 面板上注册先在 Demo 里手动 Create 出来测。注册成设计期组件需要额外写一个 Register 单元并编进设计期包改一次编一次太慢。换 Canvas 后端是另一个高频改造点。8.06 的 Canvas 是分层的你要接一个自己的渲染目标比如某个国产绘图库、某个嵌入式屏幕的专用接口只需要继承 TTeeCanvas把 MoveTo / LineTo / Polygon / TextOut 这几个纯虚方法实现掉其余几十种图形会通过基类的默认组合逻辑自动落到你的原语上。验证方法是拿一份标准的 Demo 工程把 Chart 的 Canvas 换成你的实现逐张图表比对截图——差异集中在文字排版和抗锯齿上几何位置应该完全一致。改完之后我一般会跑三个检查一是同一份 .tee 模板在新旧版本之间来回存读确认序列化没丢字段二是所有图表类型批量导出一次 PNG用像素级 diff 看有没有意外变化三是把数据量灌到十万点看内存和帧率有没有退化。最后说个习惯。自从那次被重复注册的包坑了一整天之后我每次动这类控件源码第一步都是先在一个干净虚拟机上把原始包完整编一遍、跑一个 Demo、截个图存档作为“基线”。后面所有改动都对着这张图 diff不然改到第八个单元的时候你早就忘了原始行为长什么样了。希望帮到你。本文还有配套的精品资源点击获取