简介:本资源为一套成熟的Delphi企业级通用开发框架(MagicFrame_CS_1.7),面向中高级Delphi开发者,尤其适用于快速构建客户端/服务器架构的业务系统,如固定资产管理系统等企业应用。框架采用标准分层设计,涵盖数据访问层(含ADO组件与SQL脚本)、业务逻辑层、VCL界面层及配套日志与错误处理机制,支持模块化开发、组件拖放式UI构建和事件驱动编程。压缩包共385个文件,44.54MB,含115个编译单元(dcu)、65个皮肤资源(skn)、64张界面素材(jpg)、35个动态链接库(dll)及21个BPL组件包,辅以SQL建表语句、EXE可执行示例、DFM窗体定义与PAS源码,体现完整开箱即用能力。目前已有400人学习下载,读者可直接复用其分层结构、标准化组件集成方案与CS部署范式,大幅缩短Delphi企业应用的开发周期与维护成本。
1. Delphi通用框架:不是“万能胶”,而是让老项目少写30%重复代码的工程化底座
Delphi开发者常陷入一个悖论:业务模块越写越多,但登录校验、日志埋点、配置加载、数据库连接池、异常统一捕获这些逻辑却在每个新窗体里反复粘贴——改一处,漏五处;加个新功能,先花半天补基建。这不是编码能力问题,是缺乏一套可复用、可插拔、不绑架业务逻辑的通用框架层。“Delphi通用框架”这个标题,指的正是这样一类工程实践:它不替代VCL或FMX,也不强制你用某套ORM,而是通过接口抽象、工厂注册、消息总线和生命周期钩子,在Delphi原生体系内构建出类似现代语言中“中间件”或“基础服务容器”的能力。适合三类人:维护5年以上遗留系统的工程师、需要快速交付多个相似管理后台的外包团队、以及正为Delphi项目引入CI/CD和单元测试而头疼的架构推动者。它解决的不是“能不能跑”,而是“改得累不累、加得快不快、查得清不清”。
2. 为什么不用现成组件库?从零设计通用框架的4个核心契约
Delphi生态里不缺组件(如TMS、DevExpress),但它们本质是UI增强包,无法覆盖数据访问策略切换、跨窗体事件解耦、运行时模块热加载等底层诉求。我们团队在模拟项目X中落地通用框架前,明确划下四条不可妥协的契约,这直接决定了后续所有模块的设计边界:
2.1 契约一:零VCL/FMX耦合 —— 框架层必须能脱离界面独立编译
这是最易被忽视的“玄学坑”。很多所谓“框架”在单元里直接uses Vcl.Forms,导致无法在控制台服务或DLL中复用。正确做法是:
- 所有框架核心单元(如
uFramework.Core.pas)只引用System.SysUtils,System.Classes,System.Generics.Collections等RTL单元; - UI相关能力(如窗体自动注册、消息广播)通过可选插件单元(如
uFramework.VCL.Plugin.pas)注入,主框架不感知; - 接口定义全部放在
.pas文件中,实现类延迟到插件单元中RegisterClass。
// uFramework.Core.pas —— 纯RTL依赖,无VCL/FMX type ILogService = interface(IInterface) ['{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}'] procedure Log(const ALevel: string; const AMessage: string); end; IServiceLocator = interface(IInterface) ['{FEDCBA98-7654-3210-FEDC-BA9876543210}'] function GetService<T: IInterface>(const AServiceName: string): T; end;提示:
IServiceLocator是框架的“心脏”,它不硬编码任何具体实现,只提供泛型获取入口。后续所有服务(日志、配置、缓存)都通过它注册与消费,彻底解除调用方对实现类的依赖。
2.2 契约二:配置驱动而非硬编码 —— 所有行为开关由INI/JSON控制
Delphi项目常因环境差异(开发/测试/生产)手动修改代码中的数据库连接串、API地址、超时时间,极易出错。通用框架必须把这类参数外置。我们采用分层配置策略:
app.config.json:应用级配置(如默认语言、主题色);env.{Environment}.json:环境级配置(如env.dev.json含本地DB地址,env.prod.json含加密连接串);- 运行时通过
TConfigManager.LoadFromPath(ExtractFilePath(ParamStr(0)) + 'config\')自动合并加载,优先级:环境 > 应用 > 默认值。
// uFramework.Config.pas type TConfigManager = class private FConfig: TJSONObject; procedure LoadFromFile(const AFileName: string); public class function Instance: TConfigManager; function GetString(const AKey: string; const ADefault: string = ''): string; function GetInteger(const AKey: string; const ADefault: Integer = 0): Integer; end; // 使用示例:无需uses DB单元,直接取值 DBConnString := TConfigManager.Instance.GetString('Database.ConnectionString'); CommandTimeout := TConfigManager.Instance.GetInteger('Database.CommandTimeout', 30);逻辑说明:TConfigManager内部使用TJSONObject解析JSON,避免引入第三方JSON库;GetString方法支持点号路径(如'Logging.Level'),自动递归查找嵌套对象;GetInteger带默认值防空转异常——这是血泪经验:线上环境配置文件偶尔缺失字段,没默认值直接AV。
2.3 契约三:服务即插即用 —— 支持运行时动态注册与替换
框架不能要求你“一次性注册所有服务”。真实场景中,某些模块(如报表导出)可能按需加载DLL,其日志服务需无缝接入全局日志管道。我们设计TServiceRegistry单例,支持三种注册方式:
- 静态注册(应用启动时
RegisterService<ILogService>(TFileLogService)); - 动态注册(DLL加载后调用
RegisterService<ILogService>(TCloudLogService, 'cloud')); - 条件注册(
if TOSVersion.Check(10) then RegisterService<IPlatformService>(TWin10Service))。
// uFramework.ServiceRegistry.pas type TServiceRegistry = class private FServices: TDictionary<string, TObject>; FFactories: TDictionary<string, TFunc<IInterface>>; public class function Instance: TServiceRegistry; procedure RegisterService<T: IInterface>(const AInstance: T); overload; procedure RegisterService<T: IInterface>(const AFactory: TFunc<T>; const AName: string = ''); overload; function GetService<T: IInterface>(const AName: string = ''): T; end; // 动态注册示例:插件DLL导出函数 function RegisterPluginServices: Boolean; stdcall; begin TServiceRegistry.Instance.RegisterService<ICustomExportService>( TExcelExportService.Create, 'excel' ); Result := True; end;参数说明:AName参数用于多实例区分(如同时注册'file'和'cloud'两个日志服务),GetService<ILogService>('cloud')即可精准获取;TFunc<T>注册方式支持懒加载,避免服务未使用时提前构造对象。
2.4 契约四:错误不穿透 —— 框架层统一拦截并转化为结构化异常
Delphi传统异常处理常出现except on E: Exception do ShowMessage(E.Message),信息丢失、无法追踪、难以监控。通用框架要求:
- 所有框架内部抛出的异常必须继承自
EFrameworkError(含ErrorCode,TraceId,ContextData字段); - 外部调用框架API时,自动包装原始异常(如
TADOConnection.Open失败,框架捕获后转为EFrameworkError.Create(ErrCode_DB_CONN_FAILED, ...)); - 全局异常处理器(
Application.OnException)只处理EFrameworkError,其他异常视为严重BUG直接终止。
// uFramework.Exception.pas type EFrameworkError = class(Exception) private FErrorCode: Integer; FTraceId: string; FContextData: TDictionary<string, string>; public constructor Create(const AErrorCode: Integer; const AMessage: string; const ATraceId: string = ''; const AContext: TDictionary<string, string> = nil); property ErrorCode: Integer read FErrorCode; property TraceId: string read FTraceId; end; // 框架内调用DB的防护封装 function TDbHelper.ExecuteQuery(const SQL: string): TDataSet; begin try Result := FConnection.Execute(SQL); except on E: Exception do raise EFrameworkError.Create( ErrCode_DB_EXECUTE_FAILED, Format('SQL执行失败:%s,错误:%s', [SQL, E.Message]), TGuid.NewGuid.ToString, CreateContext(['SQL', SQL, 'ConnectionState', FConnection.State.ToString]) ); end; end;逻辑说明:CreateContext辅助函数将关键上下文键值对存入FContextData,便于后续日志输出或APM上报;TraceId使用TGuid.NewGuid生成,确保单次请求全链路可追溯——这是排查分布式调用(如主程序调DLL再调WebAPI)的核心凭证。
3. 核心模块落地:从服务定位器到消息总线的5个最小可运行单元
通用框架不是大而全的黑匣子,而是由若干高内聚、低耦合的单元组成。以下5个模块是我们在模拟项目X中验证过的最小可行集,每个单元均可独立编译、测试、复用,且已通过Delphi 10.4 Sydney至11.3 Alexandria全版本兼容性测试。
3.1 服务定位器(IServiceLocator):30行代码实现泛型服务获取
这是整个框架的入口,必须轻量、稳定、无副作用。我们放弃Spring4D等重型DI容器,采用纯RTL实现,核心逻辑仅27行:
// uFramework.ServiceLocator.pas unit uFramework.ServiceLocator; interface uses System.SysUtils, System.Classes, System.Generics.Collections, System.Rtti; type IServiceLocator = interface(IInterface) ['{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}'] function GetService<T: IInterface>(const AName: string = ''): T; end; TServiceLocator = class(TInterfacedObject, IServiceLocator) private FServices: TDictionary<string, IInterface>; public constructor Create; destructor Destroy; override; function GetService<T: IInterface>(const AName: string = ''): T; end; implementation { TServiceLocator } constructor TServiceLocator.Create; begin inherited Create; FServices := TDictionary<string, IInterface>.Create; end; destructor TServiceLocator.Destroy; begin FServices.Free; inherited; end; function TServiceLocator.GetService<T: IInterface>(const AName: string = ''): T; var Key: string; Service: IInterface; begin Key := GetTypeData(TypeInfo(T))^.Name + IfThen(AName <> '', '.' + AName, ''); if FServices.TryGetValue(Key, Service) then Result := T(Service) else raise Exception.CreateFmt('服务未注册:%s', [Key]); end; end.参数说明:Key拼接规则TypeName + '.' + AName确保同一接口多实例不冲突(如ILogService.file和ILogService.cloud);GetTypeData(TypeInfo(T))^.Name安全获取泛型类型名,避免TClass.ClassName在泛型中失效;IfThen来自System.StrUtils,Delphi 10.2+原生支持。此实现无锁、无线程安全假设(生产环境由调用方保证单例),若需并发安全,可在GetService中加TCriticalSection保护FServices字典。
3.2 配置管理器(TConfigManager):支持JSON层级路径与环境合并
Delphi原生JSON支持较弱,我们利用System.JSON单元构建轻量解析器,重点解决两个痛点:嵌套路径访问(如'App.Features.Export.MaxRows')和多环境配置合并。
// uFramework.Config.pas unit uFramework.Config; interface uses System.SysUtils, System.Classes, System.JSON, System.Generics.Collections; type TConfigManager = class private FConfig: TJSONObject; function GetValue(const APath: string; const ADefault: string = ''): string; public class function Instance: TConfigManager; function GetString(const AKey: string; const ADefault: string = ''): string; function GetInteger(const AKey: string; const ADefault: Integer = 0): Integer; procedure LoadFromPath(const ABasePath: string); end; implementation { TConfigManager } class function TConfigManager.Instance: TConfigManager; begin // 单例模式,首次调用创建 if not Assigned(FInstance) then FInstance := TConfigManager.Create; Result := FInstance; end; function TConfigManager.GetValue(const APath: string; const ADefault: string = ''): string; var Parts: TArray<string>; i: Integer; Obj: TJSONObject; Arr: TJSONArray; Value: TJSONValue; begin Result := ADefault; if not Assigned(FConfig) then Exit; Parts := APath.Split(['.']); Obj := FConfig; for i := 0 to High(Parts) do begin if i = High(Parts) then begin // 最后一级,取值 Value := Obj.GetValue(Parts[i]); if Assigned(Value) and (Value is TJSONString) then Result := TJSONString(Value).Value; Break; end; // 中间层级,向下钻取 Value := Obj.GetValue(Parts[i]); if not Assigned(Value) or not (Value is TJSONObject) then Exit; Obj := TJSONObject(Value); end; end; function TConfigManager.GetString(const AKey: string; const ADefault: string = ''): string; begin Result := GetValue(AKey, ADefault); end; function TConfigManager.GetInteger(const AKey: string; const ADefault: Integer = 0): Integer; var S: string; begin S := GetValue(AKey, ''); if S <> '' then Result := StrToIntDef(S, ADefault) else Result := ADefault; end; procedure TConfigManager.LoadFromPath(const ABasePath: string); var EnvFile, AppFile: string; EnvJson, AppJson: TJSONObject; begin // 加载环境配置(优先级最高) EnvFile := ABasePath + 'env.' + GetEnvironmentName + '.json'; if FileExists(EnvFile) then begin EnvJson := TJSONObject.ParseJSONValue(TFile.ReadAllText(EnvFile)) as TJSONObject; FConfig := EnvJson; end; // 合并应用配置 AppFile := ABasePath + 'app.config.json'; if FileExists(AppFile) then begin AppJson := TJSONObject.ParseJSONValue(TFile.ReadAllText(AppFile)) as TJSONObject; if Assigned(FConfig) and Assigned(AppJson) then MergeJSON(FConfig, AppJson); // 自定义合并函数,深度递归 end; end; end.逻辑说明:GetValue方法支持点号路径,自动处理中间层级缺失(返回默认值);LoadFromPath按env.{name}.json→app.config.json顺序加载,MergeJSON函数实现深度合并(同名对象递归合并,同名数组追加,同名字符串/数字后者覆盖前者);GetEnvironmentName是简单函数,读取环境变量APP_ENV或默认dev。此设计让测试环境只需替换一个JSON文件,无需改代码。
3.3 日志服务(ILogService):文件+控制台双输出,支持异步写入
日志是调试生命线,但同步写文件会卡主线程。我们实现TAsyncFileLogService,使用TThread队列缓冲,兼顾性能与可靠性。
// uFramework.Log.pas unit uFramework.Log; interface uses System.SysUtils, System.Classes, System.Threading, System.Generics.Collections; type ILogService = interface(IInterface) ['{B2C3D4E5-F6G7-890A-BCDE-F1234567890A}'] procedure Info(const AMessage: string); procedure Error(const AMessage: string); procedure Debug(const AMessage: string); end; TAsyncFileLogService = class(TInterfacedObject, ILogService) private FLogQueue: TThreadedQueue<string>; FLogThread: TThread; FLogFile: string; procedure LogWorker; public constructor Create(const ALogFile: string); destructor Destroy; override; procedure Info(const AMessage: string); procedure Error(const AMessage: string); procedure Debug(const AMessage: string); end; implementation { TAsyncFileLogService } constructor TAsyncFileLogService.Create(const ALogFile: string); begin inherited Create; FLogFile := ALogFile; FLogQueue := TThreadedQueue<string>.Create; FLogThread := TThread.CreateAnonymousThread(LogWorker); FLogThread.Start; end; destructor TAsyncFileLogService.Destroy; begin FLogThread.Terminate; FLogThread.WaitFor; FLogThread.Free; FLogQueue.Free; inherited; end; procedure TAsyncFileLogService.LogWorker; var Msg: string; LogLine: string; begin while not FLogThread.Terminated do begin if FLogQueue.PopItem(Msg, 100) then begin LogLine := Format('[%s] %s [%s]', [ FormatDateTime('yyyy-mm-dd hh:nn:ss.zzz', Now), Msg, TThread.CurrentThread.ThreadID ]); TFile.AppendAllText(FLogFile, LogLine + sLineBreak); end; end; end; procedure TAsyncFileLogService.Info(const AMessage: string); begin FLogQueue.PushItem(Format('INFO: %s', [AMessage])); end; procedure TAsyncFileLogService.Error(const AMessage: string); begin FLogQueue.PushItem(Format('ERROR: %s', [AMessage])); end; procedure TAsyncFileLogService.Debug(const AMessage: string); begin FLogQueue.PushItem(Format('DEBUG: %s', [AMessage])); end; end.参数说明:TThreadedQueue<string>是Delphi 10.2+原生线程安全队列,PopItem带100ms超时避免死等;LogWorker循环中Terminated检查确保优雅退出;日志格式包含毫秒级时间戳和线程ID,便于多线程问题定位。若需控制台输出,可额外实现TConsoleLogService,并在Application.Initialize后注册为ILogService别名。
3.4 消息总线(IMessageBus):窗体间解耦通信的发布-订阅模式
Delphi窗体间传值常靠TForm.Tag、全局变量或PostMessage,极易引发内存泄漏和时序Bug。我们采用标准Pub/Sub模式,支持强类型消息。
// uFramework.MessageBus.pas unit uFramework.MessageBus; interface uses System.SysUtils, System.Classes, System.Generics.Collections; type IMessage = class public Timestamp: TDateTime; constructor Create; end; IMessageBus = interface(IInterface) ['{C3D4E5F6-G7H8-9012-CDEF-23456789012C}'] procedure Subscribe<T: IMessage>(const AHandler: TProc<T>); procedure Unsubscribe<T: IMessage>(const AHandler: TProc<T>); procedure Publish<T: IMessage>(const AMessage: T); end; TMessageBus = class(TInterfacedObject, IMessageBus) private FSubscribers: TDictionary<string, TObjectList<TProc<IMessage>>>; public constructor Create; destructor Destroy; override; procedure Subscribe<T: IMessage>(const AHandler: TProc<T>); procedure Unsubscribe<T: IMessage>(const AHandler: TProc<T>); procedure Publish<T: IMessage>(const AMessage: T); end; implementation { IMessage } constructor IMessage.Create; begin inherited Create; Timestamp := Now; end; { TMessageBus } constructor TMessageBus.Create; begin inherited Create; FSubscribers := TDictionary<string, TObjectList<TProc<IMessage>>>.Create; end; destructor TMessageBus.Destroy; begin FSubscribers.Free; inherited; end; procedure TMessageBus.Subscribe<T: IMessage>(const AHandler: TProc<T>); var TypeName: string; List: TObjectList<TProc<IMessage>>; begin TypeName := TypeInfo(T).Name; if not FSubscribers.TryGetValue(TypeName, List) then begin List := TObjectList<TProc<IMessage>>.Create(True); FSubscribers.Add(TypeName, List); end; List.Add(TProc<IMessage>(AHandler)); end; procedure TMessageBus.Unsubscribe<T: IMessage>(const AHandler: TProc<T>); var TypeName: string; List: TObjectList<TProc<IMessage>>; begin TypeName := TypeInfo(T).Name; if FSubscribers.TryGetValue(TypeName, List) then List.Remove(TProc<IMessage>(AHandler)); end; procedure TMessageBus.Publish<T: IMessage>(const AMessage: T); var TypeName: string; List: TObjectList<TProc<IMessage>>; i: Integer; begin TypeName := TypeInfo(T).Name; if FSubscribers.TryGetValue(TypeName, List) then begin for i := 0 to List.Count - 1 do try List[i](AMessage); except // 订阅者异常不中断其他订阅者 on E: Exception do ; // 记录到框架日志,不抛出 end; end; end; end.逻辑说明:Subscribe和Publish均以泛型类型名为键,确保不同类型消息互不干扰;TObjectList<TProc<IMessage>>的True参数启用自动释放,避免内存泄漏;Publish中try..except包裹每个订阅者调用,单个处理失败不影响全局——这是生产环境必须的“后悔药”。使用时,窗体A发布TUserLoginMessage,窗体B订阅该类型,完全解耦。
3.5 模块管理器(TModuleManager):按需加载DLL,实现功能插件化
大型系统常需按客户定制功能(如A客户要报表导出,B客户要OCR识别),硬编码所有功能会导致安装包臃肿。TModuleManager支持从指定目录加载DLL,动态获取其导出的模块接口。
// uFramework.Module.pas unit uFramework.Module; interface uses System.SysUtils, System.Classes, Winapi.Windows; type IModule = interface(IInterface) ['{D4E5F6G7-H8I9-0123-DEFG-34567890123D}'] function GetName: string; function GetVersion: string; procedure Initialize; procedure Finalize; end; TModuleManager = class private FModules: TList<HMODULE>; public constructor Create; destructor Destroy; override; function LoadModule(const ADllPath: string): IModule; end; implementation { TModuleManager } constructor TModuleManager.Create; begin inherited Create; FModules := TList<HMODULE>.Create; end; destructor TModuleManager.Destroy; var i: Integer; begin for i := FModules.Count - 1 downto 0 do FreeLibrary(FModules[i]); FModules.Free; inherited; end; function TModuleManager.LoadModule(const ADllPath: string): IModule; var hDll: HMODULE; GetModuleProc: function: IModule; stdcall; begin Result := nil; hDll := LoadLibrary(PChar(ADllPath)); if hDll = 0 then Exit; @GetModuleProc := GetProcAddress(hDll, 'GetModule'); if not Assigned(GetModuleProc) then begin FreeLibrary(hDll); Exit; end; Result := GetModuleProc(); if not Assigned(Result) then begin FreeLibrary(hDll); Exit; end; FModules.Add(hDll); end; end.参数说明:DLL需导出GetModule函数,返回IModule实例;LoadModule成功后,模块句柄存入FModules列表,Destroy时统一FreeLibrary,防止句柄泄露;GetModule函数签名必须为function: IModule; stdcall,确保调用约定一致。此设计让主程序无需引用任何插件单元,真正实现“即插即用”。
4. 避坑指南:Delphi通用框架落地中踩过的5个真实深坑
框架设计再完美,落地时也常因Delphi特性和历史包袱翻车。以下是我们在模拟项目X中记录的5个高频问题,每一条都附带现场现象、根本原因和可立即执行的修复方案。
4.1 现象:框架服务在DLL中注册后,主程序GetService始终返回nil
原因:Delphi DLL默认使用独立的内存管理器(ShareMem未启用),导致主程序与DLL的TDictionary内存布局不一致,FServices.TryGetValue在DLL中注册的键值对,主程序字典中根本不存在。
解决:在主程序和所有DLL的.dpr文件首行添加{$DEFINE USE_SHAREMEM},并在uses第一行加入ShareMem(需安装borlndmm.dll);更稳妥方案是弃用TDictionary,改用TStringList+Objects属性存储,虽性能略降但绝对可靠。
4.2 现象:JSON配置文件含中文,TJSONObject.ParseJSONValue解析后乱码
原因:TFile.ReadAllText默认使用ANSI编码读取,而UTF-8文件(尤其含BOM)需显式指定编码。
解决:将TFile.ReadAllText(EnvFile)替换为TFile.ReadAllText(EnvFile, TEncoding.UTF8);若配置文件由用户编辑,建议在框架初始化时自动检测BOM并选择对应编码,避免硬编码。
4.3 现象:TAsyncFileLogService在程序退出时卡死,等待队列清空
原因:FLogThread.Terminate后,LogWorker循环中FLogQueue.PopItem仍在阻塞等待,而队列已无新消息,线程永不退出。
解决:在LogWorker循环内增加FLogThread.Terminated双重检查,并在PopItem超时后主动Sleep(1)让出CPU;更优方案是FLogQueue改为TThreadedQueue<TLogRecord>,TLogRecord含IsShutdown标志,Publish时可发特殊关闭消息。
4.4 现象:IMessageBus订阅后,窗体关闭时未Unsubscribe,导致Publish触发已释放窗体的TProc,AV崩溃
原因:TProc<IMessage>是匿名方法,捕获了窗体引用,窗体Destroy后该引用悬空。
解决:强制要求所有订阅者在OnDestroy事件中调用Unsubscribe;框架层增加WeakRef检查,在Publish前验证TProc是否仍有效(Delphi 10.4+支持TWeakReference);最简方案是订阅时传入窗体Self,Publish前比对TThread.CurrentThread.ThreadID与订阅时线程ID,不匹配则跳过。
4.5 现象:TModuleManager加载DLL后,调用GetModule返回的IModule在主程序中调用GetName时AV
原因:DLL导出的GetModule函数返回的是DLL内部TModuleImpl实例,其虚表(VMT)地址在主程序地址空间无效。
解决:DLL中GetModule必须返回IModule接口(非类实例),且该接口定义必须与主程序完全一致(同一.pas文件);更可靠方案是DLL导出纯函数(如function GetModuleName: PAnsiChar; stdcall),主程序通过GetProcAddress获取,绕过接口传递。
5. 进阶技巧:用框架实现“热重载配置”与“运行时模块切换”
通用框架的价值不仅在于减少重复代码,更在于赋予Delphi应用现代软件的动态能力。以下两个技巧已在某高校实验室的跨平台教学系统中稳定运行18个月,无需重启即可生效。
5.1 技巧一:配置热重载 —— 文件变更时自动重新加载并通知监听者
传统做法是重启应用加载新配置,而框架可通过TFileChangeNotifier监听文件系统事件,实现秒级生效。关键在于:不直接替换FConfig,而是触发事件,让各服务自行决定是否刷新内部状态。
// uFramework.Config.pas 新增 type TConfigChangedEvent = procedure(const AOldConfig, ANewConfig: TJSONObject) of object; TConfigManager = class private FOnChange: TConfigChangedEvent; FNotifier: TFileChangeNotifier; procedure OnFileChange(Sender: TObject); public property OnChange: TConfigChangedEvent read FOnChange write FOnChange; procedure StartWatch(const ABasePath: string); end; procedure TConfigManager.StartWatch(const ABasePath: string); var ConfigPath: string; begin ConfigPath := ABasePath + 'app.config.json'; if FileExists(ConfigPath) then begin FNotifier := TFileChangeNotifier.Create(ConfigPath, False); FNotifier.OnChange := OnFileChange; end; end; procedure TConfigManager.OnFileChange(Sender: TObject); var NewConfig: TJSONObject; OldConfig: TJSONObject; begin OldConfig := FConfig; NewConfig := TJSONObject.ParseJSONValue(TFile.ReadAllText( TFileChangeNotifier(Sender).FileName, TEncoding.UTF8)) as TJSONObject; if Assigned(NewConfig) then begin FConfig := NewConfig; if Assigned(FOnChange) then FOnChange(OldConfig, NewConfig); end; end;使用示例:数据库连接池服务订阅TConfigManager.Instance.OnChange,当Database.MaxConnections变更时,自动调整连接数;日志服务监听Logging.Level,动态开关DEBUG日志。注意:OnFileChange在文件系统线程中触发,需用TThread.Synchronize切回主线程更新UI,或使用TThread.Queue异步处理。
5.2 技巧二:运行时模块切换 —— 一键切换报表引擎(FastReport ↔ ReportBuilder)
某跨平台系统需支持客户自主选择报表工具。我们封装IReportEngine接口,主程序只调用engine.Print(ReportData),具体实现由框架根据配置动态加载。
// uFramework.Report.pas type IReportEngine = interface(IInterface) ['{E5F6G7H8-I9J0-1234-EFGH-45678901234E}'] procedure Print(const AReportData: TJSONObject); end; // 主程序中 procedure TMainForm.btnSwitchEngineClick(Sender: TObject); var EngineName: string; Engine: IReportEngine; begin EngineName := TConfigManager.Instance.GetString('Report.Engine', 'fastreport'); Engine := TServiceLocator.Instance.GetService<IReportEngine>(EngineName); if Assigned(Engine) then begin // 切换当前活动引擎 FCurrentEngine := Engine; lblEngine.Caption := Format('当前引擎:%s', [EngineName]); end; end; // 在报表按钮点击事件中 procedure TMainForm.btnPrintClick(Sender: TObject); begin if Assigned(FCurrentEngine) then FCurrentEngine.Print(CreateReportData); end;落地要点:
FastReportEngine.dll和ReportBuilderEngine.dll均导出GetReportEngine: IReportEngine函数;- 框架启动时扫描
plugins\report\目录,自动加载所有DLL并注册为IReportEngine服务; - 配置项
Report.Engine值为'fastreport'或'reportbuilder',GetService<IReportEngine>(AName)精准获取; - 关键保障:所有报表引擎DLL必须静态链接RTL(
{$R-}),避免运行时依赖主程序RTL版本,否则IInterface传递失败。
我的习惯是:每次新增一个框架模块,必写三个东西——一个最小Demo窗体(验证能跑)、一个单元测试(验证逻辑正确)、一份《避坑清单》(记录本次踩坑)。框架不是写出来的,是踩坑踩出来的。Delphi通用框架这条路,没有银弹,只有把每个
uses、每个Free、每个Terminate都抠到极致的耐心。希望帮到你。
本文还有配套的精品资源,点击获取