简介:一份面向 Delphi 开发者的 D2Bridge Framework 控件包,基于 Delphi 13.1 环境,用于解决多层应用之间数据传递、组件联动与耦合度高的问题。这套框架通过桥接模式将不同数据源和应用组件连接起来,有助于提升大型项目的灵活性与可维护性。压缩包共包含 2000 个文件、容量约 369MB,其中含有 522 个 Pascal 单元源码、133 个窗体定义文件、46 个工程文件,以及 JavaScript、CSS、SCSS 等前端资源,同时带有 DLL 动态库、BPL 运行期包、可执行示例和配套文档。目前已有 71 人学习使用。包内 Demos 文件夹展示了典型应用场景的完整示例,Wizard 向导可辅助快速创建项目,DOC 与 HTML 文档覆盖 API 参考和架构说明,Unit Base 目录存放框架核心基础单元,D2Bridge Framework 主目录则集中了库文件、资源和配置文件。对于需要深入理解框架源码、希望在真实项目中应用桥接模式的中高级 Delphi 程序员,这份资源提供了从安装、配置到二次开发的完整素材。
1. D2Bridge Framework是什么:给 Delphi 13.1 控件的 Web 直译方案
业务逻辑全写在 VCL 的 OnClick 里,界面用的是 TDBGrid、TPanel 这些老控件,突然要改成浏览器访问,前端排不出人手,这是很多 Delphi 团队卡住的地方。D2Bridge Framework 的思路是把 VCL 控件按“控件名”直译成浏览器元素:TLabel 变成 div,TEdit 变成 input,TDBGrid 变成带分页的表格,窗体上的事件自动生成回发脚本,后端仍由 Delphi 进程执行业务代码。安装包是一个 zip,里面是 bpl、dcu 和 demo 工程,解压后在 Delphi 13.1 里配好路径就能编译。
这套框架不要求你会 React 或 Vue,也不要求重写数据访问层。适合手里有排产、进销存、设备监控这类老项目、准备把界面搬到浏览器交付的团队;也适合想用 Delphi 继续做 Web 服务端、又不想碰 HTTP 细节的个人开发者。下面从映射原理讲到三个高频排错点,每一步都有可复制的代码和参数。
2. D2Bridge Framework 的控件映射与回发原理
2.1 三层映射:VCL 控件树怎么变成 DOM
D2Bridge 渲染一个窗体时,不是手动拼接 HTML 字符串,而是把 Form 当作一棵控件树递归遍历。每个 VCL 控件按类名找到对应的“渲染器”,由渲染器输出 HTML 片段和配套的 CSS 类,子控件递归嵌到父容器里。这个映射关系在安装包里通常以组件映射表形式给出,常用对应关系如下。
| VCL/D2Bridge 控件 | 常用属性对应 | 输出的前端元素 |
|---|---|---|
| TaaLabel | Caption | span.d2bridge-label |
| TaaEdit | Text | input[type=text] |
| TaaComboBox | Items、ItemIndex | select + option |
| TaaMemo | Lines | textarea |
| TaaButton | Caption、Enabled | button[data-action=click] |
| TaaDBGrid | Columns、DataSource | table.d2bridge-grid |
| TaaPanel | Color、Align | div.d2bridge-panel |
从表里能看出两个特点。第一,控件名基本是“Taa + 原 VCL 类名”,这是刻意设计,方便老代码按名字替换。第二,属性不是一对一翻译,例如 TaaDBGrid 的 Columns 会被渲染成表头,而 DataSource 里的记录会走另一套 JSON 序列化逻辑,前端拿到数据后填充表格。理解这层映射,后面调列宽、间距、图表刷新时才不会对着 DOM 瞎猜。
映射过程本身也支持自定义。常见做法是继承框架的控件渲染器,重写Render方法输出自己的 HTML 模板,再把新渲染器注册到控件类型上。这样内网系统里的专用控件——比如设备状态灯、工艺流程图的 Canvas 绘制——也能保留原样出现在浏览器里。我一般会把这类自定义渲染器单独放一个单元,避免和业务窗体耦合。
2.2 请求生命周期:控件事件是如何回发的
浏览器里的按钮没有 Delphi 的TNotifyEvent,D2Bridge 的处理方式是让每个可交互控件自动挂一个事件监听器:点击后 JS 收集当前页面的表单状态,打包成 JSON,通过 XMLHttpRequest 或 fetch 发到后端,后端根据回调地址找到对应方法执行,再把需要更新的局部内容返回。这个流程和传统 AJAX 类似,但回调路由是框架在窗体创建时自动注册的。
type TLoginForm = class(TD2BridgeForm) private procedure DoLogin(Sender: TObject); public procedure OnCreate; override; end; procedure TLoginForm.OnCreate; begin inherited; // 注册浏览器访问路径,例如 http://localhost:8080/login RegisterRoute('login', 'LoginPage'); TaaEdit.Create(Self) .SetName('txtUser') .SetPlaceHolder('用户名'); TaaEdit.Create(Self) .SetName('txtPwd') .SetInputType('password') .SetPlaceHolder('密码'); TaaButton.Create(Self) .SetText('登 录') .SetOnClick(DoLogin); end;这段代码做的事是:在窗体初始化时创建一个登录页所需的两个输入框和一个按钮,并给按钮绑定后端事件DoLogin。这里的关键点是SetName,前端回发时靠控件的 Name 来定位数据,所以 Name 必须唯一且不要包含中文。RegisterRoute的第一个参数是浏览器地址后缀,第二个参数是页面标题,它同时决定了前端路由和日志里看到的路由名。
当用户点击登录按钮,请求会以 POST 方式发到类似/callback/login/b1的地址,D2Bridge 在服务端根据 Session 找到之前的窗体实例,把 JSON 里的值填回 TaaEdit 的 Text,然后调用DoLogin。你在事件里读写控件的 Text、Visible 这些属性,和 Win32 时代的编程习惯一致,差别只在:事件执行完,框架会对比渲染前后的状态差异,把变化的部分推给浏览器,而不是整页刷新。
2.3 与 TWebBroker、uniGUI 的选型差异
同样是 Delphi 做 Web,TWebBroker 是纯 HTTP 框架,没有控件映射,所有 HTML 都要自己写 Response 字符串,适合接口类应用,不适合把现有 VCL 窗体搬过去。uniGUI 是另一种成熟方案,它让 VCL 控件常驻在服务器内存里,客户端操作实时同步控件状态;代价是每个会话占用服务器资源较高,Active Form 较多时对内存和锁的调优要求也上来了。
| 对比项 | TWebBroker | uniGUI | D2Bridge Framework |
|---|---|---|---|
| 控件映射 | 无 | VCL 控件常驻内存 | 请求时重建控件树 |
| 前端依赖 | 完全手写 | 自带 JS 框架 | 自带轻量 JS 渲染器 |
| 服务器内存占用 | 很低 | 高 | 中等 |
| 原有 VCL 代码复用度 | 低 | 高 | 高 |
| 学习成本 | 需学 HTTP 编程 | 需理解会话锁 | 熟悉 VCL 即可上手 |
D2Bridge 的差异点在于“组件树不常驻”。每次请求到来时,框架重建窗体实例,从 Session 恢复控件的关键状态,执行完事件后立即释放。这样做内存压力小,但也意味着你在事件里创建的临时状态不能默认存活到下一次请求,需要显式写入 Session。这是从 Win32 转过来最容易踩的坑,后面排错章节会专门讲。
3. 用 Delphi 13.1 安装 D2Bridge Framework 并跑通最小页面
3.1 解压、配置 Library 路径与编译运行时包
拿到 D2Bridge Framework.zip 后,先解压到一个纯英文目录,比如D:\libs\D2Bridge。解压后一般能看到几个子目录:Source放源码、Lib放预编译的 dcu、Demo放示例工程、根目录下是安装说明。不建议直接解压到 Delphi 的安装目录,否则升级 IDE 时容易被清理掉,也不方便多个版本共存。
配置 Library 路径是第一步:打开 Delphi 13.1,菜单 Tools > Options > Language > Delphi > Library,在 Library path 里加入D:\libs\D2Bridge\Source和D:\libs\D2Bridge\Lib\Win32。加入后 Delphi 才能找到框架的 dcu 文件。接着打开Lib里的运行时包工程(通常是.dpk后缀),在项目管理器里选择 Build。如果只是自己用,可以不安装设计期包;想在窗体设计器里直接拖 D2Bridge 控件,才需要右键 Install 设计期包。
用命令行编译也常见,尤其在 CI 环境批量构建时:
msbuild D2Bridge.dproj /t:Build /p:Config=Release /p:Platform=Win32 /p:DCC_Define="DEBUG"/t:Build表示执行构建,Config=Release关闭调试信息,Platform=Win32指定生成 32 位 bpl,DCC_Define传入条件编译符号。注意 32 位和 64 位包不能混用:如果你的主程序是 Win64,就必须编译对应的 Win64 版本,否则报“模块 %1 中找不到入口点”之类的错误。
3.2 最小可运行工程:宿主程序加一个窗体
D2Bridge 应用的结构是“宿主程序 + D2Bridge Form”。宿主程序负责启动 HTTP 服务,Form 负责页面内容。工程文件里常见做法如下:
program LoginDemo; uses Vcl.Forms, IvyD2BridgeApp, uMainForm in 'uMainForm.pas'; var App: TD2BridgeApp; begin App := TD2BridgeApp.Create(nil); try App.Title := 'D2Bridge Login Demo'; App.Port := 8080; App.URLBase := 'app'; App.Run; finally App.Free; end; end.这里TD2BridgeApp是框架自己的 Application 宿主,Port决定 HTTP 监听端口,URLBase是访问路径前缀,Run是阻塞监听循环,按 Ctrl+C 或关闭控制台时退出。启动后浏览器访问http://localhost:8080/app/login就能看到登录页。
对应窗体单元最精简的骨架如下:
type TMainForm = class(TD2BridgeForm) protected procedure OnCreate; override; end; procedure TMainForm.OnCreate; begin inherited; RegisterRoute('login', '登录页'); TaaLabel.Create(Self).SetText('用户名'); TaaEdit.Create(Self).SetName('txtUser'); TaaButton.Create(Self) .SetText('提交') .SetOnClick( procedure(Sender: TObject) begin TaaShowMessage('欢迎 ' + TaaEdit(Self.FindComponent('txtUser')).Text); end); end;这段代码说明三个要点:RegisterRoute必须在 OnCreate 里调用,框架依赖它建立路由表;TaaButton.SetOnClick支持匿名方法,适合简单逻辑;事件里通过Self.FindComponent('txtUser')拿到控件,和 VCL 里按 Name 查找控件的方式一致,区别在于这里查的是当前请求周期重建出来的实例。
3.3 三个必调参数:Port、SSL 和 Session Timeout
初次部署最容易忽略的是参数配置,D2Bridge 的官网示例里通常都写在宿主程序里,但生产环境我一般建议单独提出来放配置文件。
| 参数 | 一般位置 | 示例值 | 说明 |
|---|---|---|---|
| App.Port | 宿主程序 | 8080 | HTTP 监听端口,和反向代理转发端口保持一致 |
| App.SSL | 宿主程序 | False | 内网测试可关,生产环境在 Nginx 层终结 SSL |
| SessionTimeout | 窗体或上下文 | 30 | 页面无操作多少分钟后会话失效,单位通常为分钟 |
| App.ThreadPoolSize | 宿主程序 | 32 | 处理回调的工作线程数,量大时酌情上调 |
App.Port不是随便改改就行。如果部署在 Linux 容器里,主机端口映射要和它对应;如果前面有 Nginx,proxy_pass http://127.0.0.1:8080里的端口也得同步改,否则页面能打开但登录回调全部 404。App.SSL我建议默认保持 False,证书配置放在 Nginx 或 IIS 上,比在 Delphi 进程里管理证书方便得多,也能避免浏览器报“证书无效”。
SessionTimeout决定用户长时间挂机后是否被踢出。调太小,操作员填单填到一半提交会跳回登录页;调太大,服务器上堆积的会话状态会缓慢增长。我一般先在测试环境用 15 分钟压测,再根据平均操作时长放宽到 30 左右。
4. 控件改造实战:DBGrid、Panel 与 Chart 的 Web 化参数
4.1 TaaDBGrid:数据网格的列、分页与行点击
老系统里最重的控件大概率是 TDBGrid。D2Bridge 提供了 TaaDBGrid,用法上保留了 DataSource 绑定,但列定义需要在代码里显式声明,不再像 VCL 那样在对象监视器里右键增加列。推荐的做法是运行时绑定:
Grid := TaaDBGrid.Create(Self) .SetName('gridOrder') .SetDataSource(MemTable) .SetReadOnly(True); Grid.AddColumn('cNo', '单号', 120); Grid.AddColumn('cName', '品名', 200); Grid.AddColumn('dDate', '日期', 100); Grid.SetPageSize(20); Grid.OnRowClick := DoRowClick;AddColumn三个参数分别是字段名、表头文字、列宽(单位是 CSS 像素)。SetPageSize(20)会让前端自动显示分页按钮,但后端数据切片要自己做,框架只把当前页需要的记录交给前端。常见做法是在表单 OnCreate 里根据 PageIndex 重新查询数据集,只取 20 条;如果一次性把全量数据塞进 MemTable,数据量大时首屏加载会明显变慢。
OnRowClick在回调里能拿到行号,要拿主键值就不要依赖显示列的序号,而是单独加一列隐藏字段,或者在数据集里把主键作为第一个字段但设Visible := False。注意:行点击事件默认不选中整行,需要高亮时要在事件里Grid.SelectedRow := AIndex,否则用户看不出当前选了哪一行。
4.2 Panel 布局与子控件间距的配置
VCL 里调控件位置靠 Left、Top 和 Align,D2Bridge 把这些转换成 CSS 布局。TaaPanel 默认输出块级 div,子控件按顺序从上往下排。要改成横向排列,需要显式设置样式,常见配置如下:
Panel := TaaPanel.Create(Self) .SetStyle('display', 'flex') .SetStyle('flex-direction', 'row') .SetStyle('gap', '12px') .SetStyle('align-items', 'center'); Panel.AddChild(EditName); Panel.AddChild(EditPassword); Panel.AddChild(BtnLogin);这里gap是子控件间距,比给每个控件单独设 margin 更省事,也不会出现相邻两个控件 margin 叠加导致间隔忽大忽小的问题。align-items: center让输入框和按钮在纵向居中对齐,视觉上比默认的 stretch 好看。需要提醒的是,Flex 布局下子控件的宽度不再按 VCL 的 Width 百分比展开,输入框要限定宽度时用SetStyle('width', '240px'),或者用flex-grow控制占比。
如果页面包含左右两栏,我一般在外层 Panel 上设display: flex,里层再套子 Panel,里层各自设flex-direction。D2Bridge 会保持父子顺序输出 DOM,所以 CSS 的层叠规则和普通网页一致,样式排查可以用浏览器开发者工具直接看元素。
4.3 Chart 控件:Series 数据绑定与重绘时机
图表在 VCL 里常用 TeeChart,D2Bridge 这类框架通常自带一个轻量图表组件。它的数据填充方式比 VCL 更接近“前端图表库”的思路:先创建序列,再逐点添加数据,最后必须调用渲染方法。
Chart := TaaChart.Create(Self) .SetName('chartTrend') .SetTitle('访问量趋势'); Series := Chart.AddSeries('pv', '页面访问量'); Series.AddPoint('2025-03-01', 128); Series.AddPoint('2025-03-02', 156); Series.AddPoint('2025-03-03', 142); Chart.Options.LegendVisible := True; Chart.Options.YAxisMin := 0; Chart.Render;AddSeries的第一个参数是序列内部名称,第二个参数是图例显示名。AddPoint的两个参数分别对应 X 轴分类和 Y 轴数值,X 轴传TDateTime或格式化后的字符串都行,但同一图表里格式要统一,混用2025/03/01和2025-03-01会出现坐标轴刻度错乱。Chart.Options.YAxisMin := 0强制 Y 轴从 0 开始,避免数据波动小时图形放大失真。
最容易犯的错是忘记调Chart.Render,或者数据集重新查询后只调了AddPoint没清空旧序列。重新绑定数据前先执行Chart.ClearSeries,再重新 AddSeries 和 AddPoint,最后Render。事件回调里如果是在循环中逐条从数据集读数据,读完一次立即 Render 一次会造成页面闪烁,正确做法是先收集到数组,循环结束后一次性填充。
5. 验证与排错:请求日志、Route 冲突和刷新问题
5.1 启用请求日志:从回发到响应的完整链路
页面能打开但登录没反应,这类问题不能靠断点乱试,第一步是确认请求有没有到后端。D2Bridge 宿主通常提供请求日志接口,常见做法是在TD2BridgeApp上挂日志事件:
App.OnRequestLog := procedure(AContext: TD2BridgeRequestContext) begin LogEvent(Format('%s %s status=%d cost=%dms', [AContext.Method, AContext.URL, AContext.StatusCode, AContext.ExecutionMs])); end;AContext.Method是 GET 还是 POST,AContext.URL是带路由的完整地址,StatusCode为 404 时说明路由没注册上,为 500 时看事件里的异常堆栈。日志文件里如果只有POST /callback/login/b1没有后续记录,说明事件方法没有执行;如果有 500,把异常类型和堆栈贴到搜索引擎里,排查效率比肉眼盯代码高得多。
5.2 Route 冲突与控件不刷新
多个窗体注册了相同的路由后缀,启动时不会报错,但后注册的会覆盖先注册的,访问时永远进的是最后一个窗体。排查方法是打开框架路由表,把当前所有注册的路由名打出来,看有没有重复。另一个更隐蔽的问题是“控件不刷新”:数据源执行过Close; Open;之后页面表格没变化。原因通常是业务代码在回调里改了数据集,但没通知前端更新。修复方式:
Grid.Refresh;Refresh会把新的数据集记录序列化后推送到浏览器。如果数据集是异步查询,执行 Refresh 时线程还没返回,页面也不会变,这时要在数据集的 AfterOpen 事件里再调用一次Grid.Refresh。刷新只推变化部分,不是整表单重绘,所以日志里看到UPDATE_GRID而不是RERENDER是正常的。
5.3 中文乱码:从数据库到浏览器的三层检查
中文乱码在 Windows 下的老项目里尤其常见,尤其是 SQLite 场景。排查按三层顺序来:第一层看数据库连接,例如 FireDAC 连接 SQLite 时设OpenMode=CreateUTF8,老库是 ANSI 编码时不要强制转换,连接串加StringFormat=Unicode更保险。第二层看 Delphi 源文件和 DFM 的保存编码,IDE 里把文件编码改为 UTF-8,尤其是窗体里有中文字面量的。第三层看 HTTP 响应头,浏览器开发者工具里检查Content-Type是否带charset=utf-8。
| 检查层 | 常见症状 | 处理位置 |
|---|---|---|
| 数据库连接 | 从 SQLite 读出的中文变成问号 | 连接串加 OpenMode=CreateUTF8 |
| 源文件/IDE | 代码里的中文字符串乱码 | 文件另存为 UTF-8 |
| 响应头 | 浏览器显示 UTF-8 乱码 | 框架设置 Response.Charset |
| 前端页面 | 个别字变成菱形框 | 检查字体 CSS,不用 SimSun 直接用系统 sans-serif |
设置完这几点再刷新浏览器,打开开发者工具的网络面板,看/callback/响应体的中文是否正常,不出乱码就可以继续下一个排错了。
本文还有配套的精品资源,点击获取