简介:面向 Delphi 与 C++Builder 开发者的 TMS VCL 控件包完整源码版,覆盖从 7 到 13 的 Delphi 版本及对应 C++Builder 环境,并针对 64 位 IDE 和高 DPI 显示做了稳定性与渲染优化。控件库包含数据网格、日程排程、富文本编辑器、功能区、图表统计、自动更新及可视化过滤面板等众多模块,适合需要深度定制界面、研究高级控件实现原理或构建企业级桌面应用的开发者。包体共两千个文件,以 Pascal 源码、窗体定义和工程文件为主体,另含图标、图片、PDF 文档、示例数据与数据库文件,压缩后约 105.53MB,目录结构清晰完整,便于按需查阅和编译。目前已有七十六人参与学习下载。通过这套源码,使用者可以获得全部控件单元实现、官方演示工程和配套资源,既能对照学习高级 VCL 组件的绘制渲染、事件模型与设计期机制,也能直接移植常用功能模块到自身项目,显著缩短二次开发周期。
1. 从 v13.6.1.0 谈起:老 Delphi 项目为什么会卡在这套 UI 包上
做 Delphi 或 C++Builder 的老开发,大概率都在某个项目里见过 TMS VCL UI Pack 的安装界面。这个控件包在 Windows 桌面端的统治力,不是靠一两篇文章吹出来的,而是靠几十个可商用控件把活干完。v13.6.1.0 这个版本号,处在 TMS 组件从传统 VCL 向现代 UI 风格过渡的节点上,既有老客户熟悉的 TAdvStringGrid、TAdvMemo,也有后来被反复使用的 TAdvGlowButton、TAdvOfficePager 这些皮肤型控件。而 FullSource 完整源码版的意思,不是给你一份能看见源码的只读副本,而是连控件内部的绘制逻辑、自绘代码、属性实现都摊开在你面前,允许你在自己的项目里调试到 VCL 底层。
我遇到过不止一个项目组,卡在表格控件重绘闪烁、打印预览样式错乱这类问题上,最后翻源码才定位到是 TMS 控件内部缓存了一个设备上下文没有释放。这种问题,闭源版根本没法查。所以这套完整源码版吸引人的地方,不是代码本身有多神秘,而是它把“黑匣子”拆开了:你可以给控件的内部方法打断点,可以看到它绘制表头时到底调用了哪些 GDI 函数,可以把它封装好的行为按自己项目的习惯改掉。对于维护老系统、需要长时间和 VCL 共存的团队来说,这个价值比新特性列表更实在。接下来我会从安装落地开始,逐步拆到控件的接入顺序、常用控件的参数设置,再讲到源码级调试和几条我实际踩过的坑。
2. 从 .7z 到 IDE 工具栏出现:源码版的安装和解包顺序
拿到TMS VCL UI Pack v13.6.1.0 FullSource 完整源码版.7z之后,第一步不是急着解压,而是先确认你本机的 IDE 版本。TMS VCL 组件按 Delphi 版本分目录编译,不同版本的 RTL 和 VCL 接口有差异,包文件(.dpk)也是按版本区分的。你装的是 Delphi 10.3 Rio 还是 Delphi 11 Alexandria,决定了你需要编译哪个 .dpk,硬来只会得到一堆“找不到 System.Win.AppleAPI”之类的报错。我一般先把压缩包解压到一个纯英文路径下,避免中文目录和空格引起编译器的“玄学”问题,然后按下面的顺序操作。
2.1 先看目录结构,再决定编译哪些运行时包
解压后不要急着双击 .dpk。打开根目录,你会看到packages、source、help、samples四个核心目录。source目录里是按控件分类的源码单元,比如AdvGrid、AdvOffice、AdvMemo、AdvEdit这些目录名直接对应控件库名称;packages目录里是按 IDE 版本组织的工程文件,里面有Delphi10.3、Delphi10.4、Delphi11这样的子目录;samples里则是官方示例工程,是验证编译是否成功最有用的参照物。
# Windows 命令行下,把压缩包解压到纯英文路径 # 注意最后不要带尾随斜杠的路径习惯,tar 在 Windows 下对路径分隔符有要求 tar -xf TMS.VCL.UI.Pack.v13.6.1.0.FullSource.7z -C C:\TMS\src这段命令用了 Windows 10 1803 以后自带的 tar 工具来解压 7z 文件。-C参数指定解压目标目录,我这里建议用C:\TMS\src这样短且没有空格的路径,可以避免后面编译时 IDE 传参过长导致的奇怪问题。如果你的系统没有 tar,也可以用 Bandizip 或 7-Zip 解压,但注意不要选“解压到文件名相同的文件夹”那种嵌套结构,不然路径会深一层,后续打开 .dpk 时容易造成 IDE 误判包名。
解压完成之后,先打开packages目录看看里面有哪些 .dpk。TMS 的编译顺序有讲究:必须先编译运行时包(RunTime packages,文件名通常是TMS*RunTime.dpk),再编译设计时包(DesignTime packages,文件名通常是TMS*DesignTime.dpk)。运行时包会被编译成bpl文件放进 IDE 的bin目录或系统的SysPath,设计时包则负责把控件图标注册到 IDE 的工具面板上。顺序反了或者只编译设计时包,面板上能看到控件,一拖到窗体上就报“Can't load package”的错误。
2.2 在 IDE 里编译 dpk 包的三个步骤
在 RAD Studio 里打开 .dpk 文件的方法不复杂,但有几个细节需要留意。打开后右键点击 Project Manager 里的包节点,选择 Build,这一步会把当前包及其依赖项全部编译一次。但需要注意的是,TMS 很多控件之间存在交叉依赖,比如TAdvStringGrid依赖TMSCompGrid的基类,所以编译前最好先确认依赖顺序。
// 打开某个示例工程前,先把搜索路径配好 // Tools -> Options -> Delphi Options -> Library -> Library path 中添加: // C:\TMS\src\source\Common // C:\TMS\src\source\AdvGrid // C:\TMS\src\source\AdvEdit这是源码版和闭源版最大体验差异的地方。闭源版安装后 IDE 会自动把编译好的 .bpl 注册进去,但源码版需要你先告诉 IDE 到哪里找 .dcu 文件。上面三行路径是 TMS 控件编译时的最小依赖集合,Common里有所有控件共享的基础工具类和自绘结构,AdvGrid是表格控件的实现单元,AdvEdit是编辑框系列的实现单元。如果你的项目只用到了 Office 系列控件,把AdvEdit换成AdvOffice目录即可,但这些路径是必须加进 Library path 的,否则编译示例工程时会有“Unit not found”的中断。
路径配好后,建议从samples里的GridDemo工程开始试编译。这个工程用了 TAdvStringGrid 的绝大部分常规功能,是快速验证包是否完整的试金石。如果这个工程能编译通过,说明运行时包基本没问题。如果编译过程中弹出找不到System.NetEncoding这类单元,别急着怀疑源码不完整,先检查你的 IDE 是否打过 Update 补丁——TMS v13 的某些版本明确要求 Delphi 10.3 以上且安装了最新的 RTL 补丁,这个坑和控件本身无关。
2.3 编译报错时的三类常见现象和处理手段
编译源码包时,最影响心情的是报错信息反复指向同一个单元名,让人误以为文件缺失。我遇到过几次比较典型的报错,第一类是“E2004 Identifier redeclared: 'TBitmap'”,这通常是源码里引用了不同版本的System.UITypes,和 IDE 自带的类型定义冲突。解决手段是在包工程文件的requires列表里,确认是否引用了rtl和vcl这两个基础包,并在 Library path 里把你加的 TMS 源码路径顺序放在 Delphi 默认路径之后。第二类是“F2613 Unit 'TMSCommon' not found”,这纯粹是 Library path 没配好,TMSCommon单元在source\Common目录下,路径没指到那里就会报这个错。第三类是“E2225 Never-build package”之类提示,意思是某个 .dpk 引用的依赖包从未编译过,这时回到packages目录,把对应的依赖包按名称顺序编译一遍。
注意:编译时弹出的“Warning: Unit 'AdvGrid' is not a framework unit”这类警告是正常的,不需要处理。TMS 控件大量调用 Windows API 的 GDI 部分,这些 API 被警告为缺少平台抽象,但不影响 Win32 目标平台的使用。
3. 接入旧项目的正确姿势:先用 Grid 和 Edit 包把界面跑起来
源码版安装好之后,最容易犯的错是把几十个 TMS 包全部加到项目的requires列表里。这样做的直接后果是最终生成的 exe 体积暴增几十 MB,启动时还要加载一堆你用不到的包,内存占用也跟着涨。TMS VCL UI Pack 不是一个大一统的包,它里面每个控件系列是独立成包的,所以正确的做法是“按需引用”。对于一个典型的进销存或桌面管理软件,最常用到的是 TAdvStringGrid(表格)、TAdvEdit(输入框)、TAdvGlowButton(按钮)这三个系列。
3.1 用一个空窗体验证最基本的控件可用性
先创建一个新的 VCL 应用程序,然后在窗体上放一个 TAdvStringGrid 和一个 TAdvEdit。这两个控件如果能在设计期正常显示、能调整列宽、能在属性编辑器里看到各自特有的属性,那说明运行时包和设计时包都已经生效。接下来写一段最基础的数据填充代码,验证运行时表现。
procedure TForm1.FormCreate(Sender: TObject); begin // 设置表格为 3 列,分别表示编号、名称、数量 AdvStringGrid1.ColumnCount := 3; AdvStringGrid1.FixedCols := 0; // 固定列设 0,让三列都能横向滚动 AdvStringGrid1.RowCount := 2; AdvStringGrid1.Cells[0, 0] := 'No.'; AdvStringGrid1.Cells[1, 0] := 'Name'; AdvStringGrid1.Cells[2, 0] := 'Qty'; // 设置列宽,单位是像素,不是字符 AdvStringGrid1.ColWidths[0] := 80; AdvStringGrid1.ColWidths[1] := 200; AdvStringGrid1.ColWidths[2] := 80; // 用行插入方式追加一条数据 AdvStringGrid1.AddRow; AdvStringGrid1.Cells[0, 1] := 'A001'; AdvStringGrid1.Cells[1, 1] := 'Widget'; AdvStringGrid1.Cells[2, 1] := '12'; // 把第 2 列设为只读,避免用户误改 AdvStringGrid1.ReadOnly := False; AdvStringGrid1.Columns[1].ReadOnly := True; end;这段代码演示的是 TAdvStringGrid 最基础的用法。ColumnCount直接指定列数,注意和ColCount不同,ColumnCount是 TAdvStringGrid 特有的属性,它和内部的 TAdvGridColumn 集合绑定,能让你设置每列的独立属性;FixedCols控制左侧固定列的数量,设为 0 可以让所有列跟随横向滚动条滚动;AddRow会自动把RowCount加 1,同时保留列结构。Columns[1].ReadOnly是针对单列设置只读,比设置整个网格只读更灵活,这在 BOM 表维护场景里很实用——数量列允许编辑,名称列锁定。
跑通这段代码后,你就有了一个可以扩展的基础:后续的排序、筛选、合并单元格都是在这个网格上叠加行为。不要一上来就追求所有高级特性,先确认控件能在你的项目里稳定运行,再谈功能增强。
3.2 TAdvEdit 的数据校验和输入控制参数
TAdvEdit 相比原生 TEdit 的优势在于它自带一组数据感知类属性,不需要额外写 OnExit 事件就能做输入范围校验。我通常会在库存录入界面里用它来约束数字输入。
// 配置一个只允许输入 0 到 9999 的整数编辑框 AdvEdit1.NumericOnly := True; AdvEdit1.MinValue := 0; AdvEdit1.MaxValue := 9999; AdvEdit1.ValidationType := vtInteger; AdvEdit1.CheckOnExit := True; // 焦点离开时触发校验 AdvEdit1.Alignment := taRightJustify; // 数字右对齐这里ValidationType := vtInteger定义了校验类型是整数,MinValue和MaxValue一起构成了范围限制。CheckOnExit设为 True 后,当用户把焦点从编辑框移走时,TMS 会自动检查当前值是否在范围内,超出就弹默认提示,不需要你自己写if then判断。把Alignment设成taRightJustify是财务单据界面的常见习惯,数字右对齐方便纵向比较。这些属性在闭源版里也一样能用,但源码版能让你看到CheckOnExit内部调用的是哪个 Windows 消息,在特殊控件嵌套(比如放在 TFrame 里)时更容易排查焦点移交问题。
3.3 不要在项目里一股脑引用所有包的三个理由
很多第一次用源码版的人,看到packages目录里几十个 dpk 就产生一种“全编了才不亏”的错觉。我建议克制住这个冲动,理由有三点:第一,TMS 每个包都有独立的初始化逻辑,全量引用会让程序启动时多出几百毫秒的加载时间,对老机器上的客户端软件来说体感很明显;第二,包的依赖关系不是线性的,某些包会触发对第三方组件(如 Devart 或 FastReport)的依赖,不装这些第三方库时编译直接失败;第三,维护视角上,全量引用后如果某个包需要升级或打补丁,你要重新编译的包数量会翻倍。我自己的习惯是维护一个“白名单”列表,只保留当前项目用到的包名,新需求来的时候先看看能否用已有控件实现,实在不行才把新包加进来。
提示:TMS VCL UI Pack 里的 TAdvStringGrid 和标准 TStringGrid 在内存模型上不是一回事,前者内部用 TList 管理行对象,后者用二维数组。如果你在代码里把 AdvStringGrid 当普通 StringGrid 用,比如直接调
Rows[1].Add,会发现在某些版本里这个方法不存在,需要改用AddRow或InsertRows。
4. 从“能跑”到“好用”:TMS 控件批量接入时的布局与配置顺序
把单个控件放到窗体上验证功能,这个环节人人都能完成。真正让项目进度拉开差距的,是几十个窗体成批迁移到 TMS 控件时,布局策略、属性统一配置、资源复用这些系统性问题。我见过一个项目组把所有 TAdvEdit 的Text属性清空,然后跑到 FormShow 里一个个赋值,代码写了几百行,后来发现 TMS 的TAdvEdit.Text在TextChanged事件里会触发格式化操作,批量赋值时性能很差。这类问题不是控件本身的 bug,而是使用方式没有利用好它的事件机制。
4.1 控件命名规范和事件分发:别让 OnChange 到处散落
当一个窗体上有四五个 TAdvEdit 时,每个编辑框都挂一个 OnChange 事件处理器,代码维护成本会快速上升。我一般会用一个统一的OnEditorChange事件分发方法,在事件参数里判断是哪个编辑器发出来的。TMS 的 TAdvEdit 继承了TEdit的消息机制,所以它的Sender参数是有效的,可以直接用TComponent(Sender).Name判断来源。
procedure TForm1.OnEditorChange(Sender: TObject); var Edt: TAdvEdit; FieldName: string; begin if not (Sender is TAdvEdit) then Exit; Edt := Sender as TAdvEdit; // 根据编辑框的名称映射业务字段 FieldName := Edt.Tag.ToString; // 我在设计期把 Tag 设为字段序号 case FieldName.ToInteger of 1: FOrderData.Qty := Edt.FloatValue; // FloatValue 返回数值,比 StrToFloat 安全 2: FOrderData.Price := Edt.FloatValue; 3: FOrderData.Amount := Edt.FloatValue; end; end;Tag属性是 Delphi 所有组件自带的整数标签。我在这里让它在运行时充当字段序号,这样多个 TAdvEdit 可以共用一个事件处理入口。在窗体上有大量输入框时,这种写法的可维护性比到处挂独立事件高得多。注意这里用的是Edt.FloatValue而不是StrToFloat(Edt.Text)因为 TAdvEdit 内置了数值转换逻辑,当NumericOnly或者ValidationType配置好之后,它返回的值一定是可以安全参与计算的数字,不会因为用户输入空字符串或千分位格式而抛异常。
4.2 表格列配置的三种状态:可编辑、只读、通过下拉选择
TAdvStringGrid 的列状态管理,是我见过项目组最容易用混的地方。Columns[].ReadOnly只是控制用户是否能直接输入,不控制下拉框是否能弹出;而Columns[].Editor才是决定编辑器的类型。要通过下拉列表选择数据,需要为列指定TAdvEditColumn或者TAdvComboBoxColumn这类的编辑器对象。
var ComboCol: TAdvComboBoxColumn; begin // 在第三列加入下拉选择编辑器 ComboCol := TAdvComboBoxColumn.Create(AdvStringGrid1); ComboCol.Name := 'StatusCol'; ComboCol.Items.Add('Pending'); ComboCol.Items.Add('Approved'); ComboCol.Items.Add('Rejected'); ComboCol.DropCount := 3; // 下拉列表显示三行,超过则需要滚动 AdvStringGrid1.Columns.Insert(2, ComboCol); end;这一段代码的关键点是TAdvComboBoxColumn是作为一个独立的列对象插入到Columns集合中的,而不是通过设置某个属性完成的。创建后必须调用Columns.Insert把它挂到指定索引位置,否则它只是一个孤立对象,表格不会显示下拉效果。DropCount控制下拉区域一次性显示的行数,条目超过这个值会出滚动条。这个用法的好处是,下拉框的数据源可以动态增减,不需要像传统 VCL 那样在单元格里拼字符串。
4.3 按窗体用途划分三类页面:查询页、编辑页、报表页
另外一个容易让项目后期变得难维护的原因,是不管什么窗体都用同一套 TMS 控件属性配置。查询页的重点是快速出数据和列排序,编辑页的重点是输入约束和联动校验,报表页的重点是打印样式和列宽自适应。TMS 里的 TAdvStringGrid 针对这三类场景有各自的属性组。查询页建议开启Filter相关的功能并关闭 cell 编辑;编辑页关闭列排序和筛选,避免用户误点表头触发数据错位;报表页则需要配置PrintSettings和AutoSize相关选项。我一般会写三个基类窗体,把每种场景的默认属性预置好,业务窗体只继承对应的基类,这样一来新窗体从创建到控件行为正确,时间能从半天压缩到一两个小时。
5. 避坑手册:从编译器报错到运行期异常的 5 条踩坑记录
源码版控件,安装后最大优势是能深挖问题,但代价是有更多机会踩到底层地雷。这些坑看起来各自独立,根源往往都指向控件的内部资源管理和 IDE 版本差异。这里整理我在不同项目中实际遇到过的 5 类代表性故障,每条都按现象、原因、解决三个步骤来写。
5.1 新增一个 TAdvStringGrid 后 IDE 直接崩溃
现象:在新窗体上放置控件时,点一下工具栏图标 RAD Studio 就发生 Access Violation,窗体编辑器打不开;删掉该控件依赖的包后在别的机器上又复现。原因:设计方案时,TMS 包在Register过程中调用了Classes.RegisterClass对内部类做全局注册,而这个注册过程在 IDE 启动时和其他插件冲突。解决:先确认安装版本和 IDE 的 Update 级别匹配;其次检查 Tools > Options > VCL Designer 里的“Enable runtime theme”设置,这个选项被 TMS 自绘引擎依赖,关闭后会导致控件在 IDE 中初始化崩溃;最后,如果仍然复现,按Win + R输入%APPDATA%\Embarcadero,找到对应版本目录下的.dsk文件备份后删除再重开 IDE。
5.2 编译项目时提示“Unit 'AdvGrid' is not a framework unit,仍继续”警告后,程序界面空白
现象:编译通过,运行也启动,但窗体上的区域一块空白,点击按钮无响应。原因:这是 TMS 的智能链接机制在起作用,AdvGrid单元被链接器排除,因为它在项目中没有任何显式引用的位置。源码版和闭源版在.bpl包的Package声明上有一个细微差异:源码版的运行时包默认不强制绑定所有单元,当你的项目不是通过包文件引用 TMS,而是通过uses引用某个.pas文件时,链接器会认为控件没有引用到完整依赖树。解决:在项目选项的Runtime Packages里,把 TMS 的运行时包名加进requires列表,例如TMSGridRunTime,让工具知道它需要把整套网格单元链接进 exe。
5.3 TAdvEdit 设置了 MinValue 和 MaxValue 但校验不生效
现象:运行界面里用户可以输入超过 MaxValue 的数字,焦点移走时没有报错。原因:CheckOnExit := True只是触发了校验流程的前半段,后半段是ValidationType必须与控制类型一致;当ValidationType := vtInteger而用户输入了'12.5',TMS 会先尝试字符串转整数,失败后直接放弃校验而不是提示。解决:把ValidationType改成vtFloat或保留vtInteger但把EditMask也加好,比如EditMask := '9999',这样非法字符在输入阶段就被拦截。不要依赖MinValue单独工作,这两个属性必须配合。
5.4 AdvStringGrid 导出 Excel 时中文表头变成乱码
现象:调用SaveToXLS导出后打开 Excel,表头是乱码,数据单元格内容正常。原因:TMS 的 XLS 导出路径内部使用 GBK 编码生成字符串,而新版 Excel 默认按 UTF-8 解析sharedStrings.xml。这个现象在闭源版和源码版里都存在,但源码版里你能直接定位到XLSExport单元中的TStringList.SaveToFile调用。解决:用源码版时,可以在SaveToFile前手动改编码:
var SL: TStringList; begin SL := TStringList.Create; try SL.LoadFromFile('export.tmp'); // 让 TMS 把内容写入临时文件 SL.SaveToFile('final.xls', TEncoding.UTF8); // 重新按 UTF-8 保存 finally SL.Free; end; end;这里的核心点是先让 TMS 的导出逻辑跑完,再用 TStringList 读回来做一次编码转写。直接用TEncoding.UTF8覆盖保存会把 BOM 头也写入,Excel 识别 BOM 后会正确处理中文。如果你不想引入这个额外步骤,在 TMS 官方的数据导出组件分支里强制指定XMLEncoding也可以,但代码侵入会更大。
5.5 用 FullSource 编译示例工程时出现 “Fatal: File not found: 'System.NetEncoding.dcu'”
现象:无论怎么调整 Library path,示例工程都编译不过,报错定位到System.NetEncoding。原因:这个不是 TMS 工程本身的问题,而是 Delphi 安装时没有勾选对应平台的 RTL 源码。TMS v13 部分示例工程引用了System.NetEncoding来处理 JSON 和网络流,而该单元在 Delphi 10.3 的 RTL 中是有条件编译的,只有安装了Windows SDK或启用了System.Net相关单元时才生成.dcu。解决:打开Tools > Options > Environment Variables,确认PLATFORM是Win32,然后在Tools > Options > IDE > Rebuild里重新编译 RTL。如果还不行,直接在示例工程里把System.NetEncoding从uses中移除——示例工程里对它的引用通常只是为了 JSON 序列化演示,删掉后控件的核心功能不受影响。
6. 源码版的核心利润点:翻源码找边界,再用断点验证
把“FullSource 完整源码版”和普通二进制版区分开来的,不是代码文件的下载体积,而是你在后续开发和维护时能拿它干什么。普通版遇到问题只能通过属性和事件配置去规避,源码版则允许你直接给TAdvGridBase.ValidateEditCell这类内部方法下断点,看清它在用户输入时的完整调用链。但要提醒一句:源码版不是拿来改源码的——除非你想永久维护一个内部 fork,否则更明智的做法是通过源码读懂边界,再把配置写在事件里。
具体到实际项目,源码版最有价值的功能体现在三方面:第一,定位绘制闪烁和自定义绘制的解锁点。比如 TAdvStringGrid 的自绘表头,你可以翻到DrawColumnHeader方法,看到它内部对Canvas.Brush和Canvas.Font的处理次序,然后就能解释为什么在某些缩放比例下表头文字发虚。第二,搞懂控件的资源生命周期。TMS 源码里大量使用TComponent的Owner机制,如果你在运行时去掉窗体上的控件(比如FreeAndNil了一个 AdvGrid),但没有把子编辑器对象释放,源码里能看到TAdvGridColumnList的析构函数里是否有遍历释放代码,这是排查内存泄漏的硬依据。第三,属性修改后能不能 hot patch。在编译后的 exe 里,你改不了属性默认值,但源码版能在 IDE 中临时修改某个属性的 Default 数组再编译,快速验证一个新方案而不影响整体使用。
验证源码包是否完整的最直接方法,是在 IDE 的Project Manager里右键点击包名,选择View Source。如果能打开一个包含Register过程和类声明列表的.pas文件,基本说明这是真正的完整源码分发版;如果只能看到.bpl类型的二进制包,那就不是。对于长期维护 Windows 客户端项目的团队来说,源码版的价值不是让你去重写控件,而是让你在遇到问题时有资格问“它内部到底干了什么”——这对于缩短排查时间和提高技术判断的可信度非常关键。希望这份从安装到排障的路径对你有用,也让你的 TMS 项目少走几趟弯路。
本文还有配套的精品资源,点击获取