资讯动态

VCL老项目换肤:AlphaControls安装与WebBrowser适配

发布时间:2026/10/27 15:25:34 来源:尧图企业网站定制
简介面向 Delphi 与 CBuilder 开发者的 AlphaControls 14.24 界面组件包汇集 VCL 皮肤控件及相关源码、示例与编译资源适合需要为桌面程序快速套用专业皮肤、研究控件实现或进行二次定制的开发者。压缩包共 1055 个文件约 21.52MB包含大量 asz 皮肤文件、pas 源码、res 资源、dpk/bpk 安装包、dproj/cbproj 工程及 dcu、dfm、exe 等编译与演示文件覆盖组件库从皮肤素材到工程构建的主要环节。已有 292 人学习或下载。此版本含全部组件并激活 WebBrowser 皮肤、去除商业皮肤提示便于直接查看皮肤效果与组件集成方式借助源码、资源与示例工程读者可了解 AlphaControls 的目录组织、控件安装配置及皮肤替换思路为 Delphi 桌面项目的美化与二次开发提供较完整的参考。1. 老 VCL 项目要看起来新这套包解决的是什么维护一套跑了十来年的 Delphi 管理端最怕的不是业务逻辑出问题是客户换了 4K 显示器之后那句这软件看着像上个世纪的。推倒重写成 FMX 或者前端方案工期和风险都扛不住剩下的路基本只有一条给现有 VCL 窗体套一层皮肤引擎。AlphaControls 这套 14.24 的懒人包干的就是这件事——组件是齐的设计期能直接拖重点是里面 WebBrowser 那块的内嵌浏览器外观和商业皮肤提示都已经处理过装完不用再对着标题栏上的水印发愁。适合手上还压着 Delphi 7 到 11 的 VCL 工程、需要在一两周内交出一版看着像新做的界面的开发者项目已经迁到 FMX 或者纯 Web 的这东西对你没什么价值。2. 拆包到 IDE 面板先编运行期包再装设计期包的顺序不能反2.1 解压后先看目录哪些必须编哪些只是摆设懒人包解压出来的结构大同小异先花两分钟把目录过一遍再动手能省掉后面一半的返工。目录 / 文件内容是否必须Source\全部.pas源文件必须路径要进 Library PathPackages\按 IDE 版本分的.dpk/.dproj必须安装入口Skins\.asz皮肤文件必须要随程序一起发布Demos\/Examples\示例工程可选但排错时价值很高Help\/Docs\帮助文档可选第一个容易翻车的地方有人图省事把整个解压目录递归加进 IDE 的搜索路径。结果是Source里的单元和系统里旧版本的同名单元撞车编译报一堆莫名其妙的不兼容类型。正确做法是只把Source目录和编译产物输出目录加进去别的一概不加。第二个地方是包的分类。AlphaControls 这类库通常拆成两类包运行期包只含组件实现负责让程序跑起来设计期包requires了designide负责把组件图标画到面板上、把属性编辑器挂上去。判断哪个是哪个不用打开 IDE看 dpk 文件的 requires 段就行。# 在 Packages 目录下执行Git Bash 或 WSL 里跑 # 依赖 designide 的一定是设计期包必须最后装 for f in *.dpk; do if grep -qi designide $f; then echo [设计期] $f else echo [运行期] $f fi done这段脚本只做一件事把每个 dpk 的requires段翻出来看有没有designide。有就是设计期包没有就是运行期包。参数上没有需要改的地方唯一要注意的是必须在Packages目录下执行脚本用的是相对路径通配。跑完你会得到一份清单按运行期在前、设计期在后的顺序装就行。还有一种情况同一个 IDE 版本目录下可能同时存在 VCL 版、数据库扩展版、第三方控件适配版好几套包。桌面 VCL 项目只需要 VCL 那一套其余的先别碰装多了只是给自己增加卸载成本。2.2 编译与安装Build 和 Install 是两回事顺序错了会怎样设计期包在编译时会去链接运行期包导出的符号运行期包没编出来设计期包直接报找不到单元或者无法解析的外部符号。所以流程固定成四步。第一步关掉 IDE 里所有打开的工程尤其是那些正在引用旧版控件的。旧工程持有旧 bpl 的句柄新包覆盖时会提示文件被占用。第二步打开运行期包的 dpk在 Project Manager 里右键 Build。这里建议先 Build 再 Install而不是直接 Install——Install 会顺带编译但错误提示经常被截断先 Build 能看到完整的编译日志。第三步打开设计期包右键 Install。成功的话 IDE 会弹一个框告诉你某个 bpl 已经注册。第四步如果第三步报 Cannot load package xxx.bpl把缺失的包名记下来回到第二步补装别硬点忽略。如果要给 CI 或者多人协作环境做无人值守编译用 msbuild 更稳# 路径按自己机器的 IDE 安装位置改 MSBUILDC:/Program Files (x86)/Embarcadero/Studio/22.0/bin/msbuild.exe # 先编运行期包Release 配置Win32 平台 $MSBUILD gsPackR.dproj /p:ConfigRelease /p:PlatformWin32 /t:Build # 再编设计期包 $MSBUILD gsPackD.dproj /p:ConfigRelease /p:PlatformWin32 /t:Build/p:ConfigRelease决定编译配置/p:PlatformWin32是关键参数——设计期包在绝大多数版本里只提供 Win32 版本因为 IDE 本身是 32 位进程。你的业务程序可以编 Win64但设计期包必须编 Win32这点搞混了会得到一个组件面板上能看到、拖到 64 位工程里就报错的诡异状态。编译产物目录也要统一。在包的工程选项里把 Unit output directory 设成一个独立目录比如..\..\Lib\D12别用默认的.\$(Platform)\$(Config)。原因很简单默认目录分散在各个包下面以后升级版本时旧 dcu 清不干净Delphi 会优先用搜索结果里排在前面的那个于是你明明换了新包编译出来的还是旧行为。2.3 装完的三步自检别等写代码时才发现没装上装完立刻验证比写到一半发现组件是空壳要好得多。第一看组件面板。应该多出一个页签通常叫 AlphaSkins 或 AlphaControls里面能看到TsSkinManager、TsSkinProvider、TsWebBrowser这几个核心组件。只看到一两个说明包没装全。第二新建一个空白 VCL 工程从面板拖一个TsSkinManager上去把Active设成True。如果此时弹出找不到皮肤文件说明SkinsDirectory没指对不是安装问题是配置问题。第三切换皮肤看商业提示。这一步是懒人包的核心价值所在——循环切五六个皮肤观察标题栏区域有没有残留的提示文字或者水印。写个小循环比手动点快得多// 遍历 Skins 目录逐个应用用来肉眼检查皮肤是否完整、提示是否残留 procedure TForm1.btnScanSkinsClick(Sender: TObject); var SR: TSearchRec; Dir: string; begin Dir : IncludeTrailingPathDelimiter(SkinManager1.SkinsDirectory); if FindFirst(Dir *.asz, faAnyFile, SR) 0 then try repeat // SkinName 不带扩展名只取文件名部分 SkinManager1.SkinName : ChangeFileExt(SR.Name, ); Application.ProcessMessages; // 让重绘走完否则看不到中间态 Sleep(150); // 留一点时间肉眼观察 until FindNext(SR) 0; finally FindClose(SR); end; end;代码里SkinName只要文件名不要扩展名这是 AlphaControls 的约定写成xxx.asz会加载失败。Application.ProcessMessages不能省皮肤切换是异步重绘的不加这一句你看到的是最后一次的结果。Sleep(150)是给人眼留的观察窗口正式代码里要去掉。最后再加一个发布前的文件检查确认运行时真正加载的 bpl 是这次编出来的// 列出程序目录下的 bpl 及编译时间确认没有混入旧版本 procedure TForm1.btnListBplClick(Sender: TObject); var SR: TSearchRec; Path: string; begin Memo1.Clear; Path : ExtractFilePath(ParamStr(0)); if FindFirst(Path *.bpl, faAnyFile, SR) 0 then try repeat Memo1.Lines.Add(Format(%s | %s | %d KB, [ SR.Name, DateTimeToStr(FileDateToDateTime(SR.Time)), SR.Size div 1024])); until FindNext(SR) 0; finally FindClose(SR); end; end;时间戳全都对得上本次编译时间才说明发布目录是干净的。任何一个 bpl 的时间是半年前的都意味着有旧版本混在里面。3. 皮肤挂载三层结构TsSkinManager、TsSkinProvider 与窗体重绘的配合3.1 SkinManager 的落点DataModule 单例别每个窗体放一个TsSkinManager是整个皮肤系统的根它持有皮肤资源、维护全局的绘制钩子。常见的错误做法是每个窗体上放一个各自Active : True。后果是同一个皮肤文件被加载 N 次一个.asz通常几百 KB 到几 MB窗体一多内存直接翻倍而且多个管理器同时 Hook 窗体的WM_NCPAINT重绘顺序不确定表现为切换窗体时标题栏闪一下或者留下残影。正确做法是放在 DataModule 里全程序一个实例在 dpr 里最先创建program MyApp; uses Vcl.Forms, uDM in uDM.pas {DM: TDataModule}, uMain in uMain.pas {FormMain}; {$R *.res} begin Application.Initialize; Application.CreateForm(TDM, DM); // 皮肤在这里激活 Application.CreateForm(TFormMain, FormMain); Application.Run; end.DataModule 的OnCreate里做两件事顺序不能反procedure TDM.DataModuleCreate(Sender: TObject); begin // 皮肤目录必须是绝对路径末尾带分隔符 SkinManager1.SkinsDirectory : ExtractFilePath(ParamStr(0)) Skins\; // 先激活再让业务窗体创建 —— 反过来会有一帧的系统灰闪过 SkinManager1.Active : True; end;SkinsDirectory用ExtractFilePath(ParamStr(0))而不是写死路径否则客户机装在 D 盘就找不到皮肤。末尾的分隔符建议用IncludeTrailingPathDelimiter拼手动写反斜杠在字符串转义里容易漏。Active放在窗体创建之前是为了避免程序启动瞬间闪一下系统灰然后变皮肤的观感问题。如果业务窗体创建很慢比如主窗体上有大量控件也可以在主窗体OnShow之后再激活牺牲一点观感换启动速度这个取舍后面第 6 章会展开。3.2 SkinProviderAllowExtBorders 该开还是该关TsSkinManager管全局资源具体到每个窗体还需要一个TsSkinProvider它负责接管这个窗体的非客户区绘制——标题栏、边框、系统按钮。在对象检查器里把它的SkinData指向 SkinManager 暴露的皮肤数据不同小版本的属性名可能是SkinData或直接引用SkinManager以 IDE 里实际出现的为准。真正需要斟酌的是AllowExtBorders。这个属性打开后窗体的外边框和标题栏完全由皮肤引擎自绘好处是视觉上和皮肤主题彻底统一坏处是会跟 Windows 的 DWM 阴影、Aero Snap 贴边、高 DPI 缩放打架。我一般按窗体类型区别对待窗体类型AllowExtBorders原因主窗体开标题栏统一最大化时不会出现系统标题栏叠在皮肤上模态对话框关保留系统阴影和居中定位视觉上更稳无边框 Splash关BorderStylebsNone即可开了反而多一圈边停靠面板宿主开需要自绘边框和皮肤主题对齐开了扩展边框之后有一类问题几乎必然遇到最大化时标题栏顶部多出一条白边或者左右两侧各少一像素。根因是皮肤引擎按 96 DPI 算的非客户区尺寸和实际缩放比例对不上。处理办法是在程序启动时统一 PPI 缩放让皮肤引擎和 VCL 用同一套比例// dpr 里Application.Initialize 之前调用 procedure SetupDPI; begin // 96 是基准实际取系统 DPIAlphaControls 会据此换算边框厚度 SkinManager1.ScaleRatio : Screen.PixelsPerInch / 96; end;如果项目对高 DPI 要求不严最省事的方案是把进程声明成 DPI Unaware让系统整体位图拉伸。糊是糊一点但不会有错位交付周期紧的时候这是个合理的妥协。3.3 换肤落盘ini 读写与失败回退用户换了一次皮肤下次打开还是原来的这种体验很差。持久化本身不难难在配置文件放哪。不要放在程序目录——程序装在Program Files下时没有写权限TIniFile.Create会静默失败或者抛异常。正确位置是%APPDATA%。function ConfigPath: string; var Base: string; begin Base : GetEnvironmentVariable(APPDATA); if Base then Base : ExtractFilePath(ParamStr(0)); // 极端情况下的兜底 Result : IncludeTrailingPathDelimiter(Base) MyApp\ui.ini; ForceDirectories(ExtractFileDir(Result)); end; procedure TDM.LoadSkin; var Ini: TIniFile; Name: string; begin Ini : TIniFile.Create(ConfigPath); try Name : Ini.ReadString(UI, Skin, Office2019White); finally Ini.Free; end; // 文件不存在就回退到默认皮肤不要让程序起不来 if FileExists(SkinManager1.SkinsDirectory Name .asz) then SkinManager1.SkinName : Name else SkinManager1.SkinName : Office2019White; SkinManager1.Active : True; end;ReadString的第三个参数是默认值用户第一次运行没有配置文件时走这个分支。FileExists那一层判断是必须的——用户可能在 A 机器上选了某个皮肤把 ini 拷到 B 机器而 B 机器的皮肤包里没这个文件没有这层判断就是启动即崩。3.4 运行时开关皮肤先后次序错了会留黑块有些场景需要临时关掉皮肤比如截图导出、打印预览、嵌入到宿主程序里。直接一句SkinManager1.Active : False是不够的各个窗体的TsSkinProvider还挂着钩子结果是窗体边框恢复系统样式、标题栏还是皮肤画的中间夹一块没重绘的黑区。正确的次序是先关窗体的 provider再关 managerfunction FindSkinProvider(AForm: TCustomForm): TsSkinProvider; var I: Integer; begin Result : nil; for I : 0 to AForm.ComponentCount - 1 do if AForm.Components[I] is TsSkinProvider then Exit(TsSkinProvider(AForm.Components[I])); end; procedure TDM.SetSkinEnabled(AValue: Boolean); var I: Integer; P: TsSkinProvider; begin if not AValue then begin // 关先摘窗体的钩子再关全局 for I : 0 to Screen.FormCount - 1 do begin P : FindSkinProvider(Screen.Forms[I]); if P nil then P.Active : False; end; SkinManager1.Active : False; end else begin // 开反过来先启全局再恢复窗体 SkinManager1.Active : True; for I : 0 to Screen.FormCount - 1 do begin P : FindSkinProvider(Screen.Forms[I]); if P nil then P.Active : True; end; end; end;FindSkinProvider遍历ComponentCount而不是ControlCount因为TsSkinProvider是非可视组件不在控件树里。关和开的遍历顺序相反这不是玄学是因为 manager 的Active改变会触发所有已注册 provider 的重绘请求顺序一致才不会出现半绘制状态。4. WebBrowser 皮肤只生效一半ActiveX 子窗口的边界与绕行方案4.1 为什么 TWebBrowser 永远只被套了个壳这是问得最多的一个问题皮肤明明激活了为什么内嵌浏览器那块还是灰扑扑的系统滚动条要理解这个得知道TWebBrowser到底是什么。它是SHDocVw里对 IE 内核 ActiveX 控件的封装底层是 MSHTML 渲染引擎加上一个独立的子窗口窗口类是Internet Explorer_Server。这个子窗口由 IE 自己创建、自己绘制VCL 的皮肤引擎走的是WM_PAINT/WM_NCPAINT消息链消息到宿主窗口就断了进不到 ActiveX 内部。所以皮肤化 WebBrowser 能覆盖的只有三层宿主窗口的外边框和标题栏——能皮肤化滚动条——需要控件本身重写原生TWebBrowser不行网页内容区域——永远是 IE 画的改不了右键菜单和 alert 弹框——默认是 IE 的可以用IDocHostUIHandler换掉判断当前是哪种状态有个简单办法拿 Spy 或者 WinSpy 挂上去看窗口树如果Internet Explorer_Server这个类名的窗口还在说明内容区就是原生的皮肤只能管到外面那圈。4.2 换成 TsWebBrowser改动量比想象中小AlphaControls 在 AlphaSkins 面板里提供了一个TsWebBrowser它是TWebBrowser的派生类重写了滚动条的绘制并且能挂SkinData。替换成本很低// 替换前 uses SHDocVw; FBrowser: TWebBrowser; // 替换后 uses gsWebBrowser; // 单元名以包内实际的为准 FBrowser: TsWebBrowser;TsWebBrowser继承了TWebBrowser的全部方法和事件Navigate、GoBack、Document这些调用一行都不用改。只需要额外把皮肤数据挂上去procedure TFormMain.InitBrowser; begin // 挂上皮肤数据滚动条和边框走皮肤绘制 WebBrowser1.SkinData : SkinManager1.SkinData; WebBrowser1.Navigate(about:blank); end;SkinData这一行的属性名在不同小版本里可能是SkinData或者需要通过SkinProvider传递以对象检查器里能选到的为准。挂上之后的效果是滚动条变成皮肤主题的样式但页面内容区的白底还是 IE 的这个改不了也不应该改。如果出于某种原因不能换控件比如工程里已经有大量TWebBrowser的强类型引用退一步的做法是至少让弹框和右键菜单皮肤化——在TsSkinManager.Options里把和对话框、主菜单相关的开关打开不同版本属性名有差异常见的是soSkinDialogs、soSkinMainMenus这类弹出的确认框就不会那么突兀了。4.3 用 app:// 协议把前端事件送回 Delphi内嵌浏览器最常见的需求是页面上的按钮点了要触发 Delphi 里的逻辑。直觉做法是window.external加IDocHostUIHandler但那套 COM 接口实现起来代码量不小。更省事的是自定义协议拦截让页面跳转到一个不存在的协议在BeforeNavigate2里拦下来改成调用本地方法。const APP_SCHEME app://; procedure TFormMain.BrowserBeforeNavigate2(ASender: TObject; const pDisp: IDispatch; var URL, Flags, TargetFrameName, PostData, Headers: OleVariant; var Cancel: WordBool); var Cmd, Arg: string; P: Integer; begin // 不是自定义协议就放行正常导航 if not SameText(Copy(URL, 1, Length(APP_SCHEME)), APP_SCHEME) then Exit; Cancel : True; // 阻止 IE 真的去解析这个 URL Cmd : Copy(URL, Length(APP_SCHEME) 1, MaxInt); Arg : ; P : Pos(?, Cmd); if P 0 then begin Arg : Copy(Cmd, P 1, MaxInt); Cmd : Copy(Cmd, 1, P - 1); end; if SameText(Cmd, openorder) then OpenOrder(Arg) // 业务方法参数是订单号 else if SameText(Cmd, close) then Close; end;页面端只需要写a hrefapp://openorder?id123打开订单/a点击后 IE 会发起导航BeforeNavigate2拿到 URLCancel : True拦住真实跳转剩下的就是普通字符串处理。Arg里拿到的是原始 query string没有做 URL 解码。如果参数里可能带中文或者特殊字符记得在业务方法里先TNetEncoding.URL.Decode一次。另外BeforeNavigate2是在 UI 线程的 COM 回调里执行的里面不要做耗时操作要开线程就TThread.Queue回去。4.4 内核渲染模式和 DPI两个必须一起处理的问题TWebBrowser用的是机器上装的 IE 内核默认渲染模式取决于一堆注册表项和文档 doctype。现实情况是很多机器上它停在 IE7 的怪异模式你写的 flex 布局、CSS 变量全部失效页面看起来像 2008 年的产物。控制这个行为的是FEATURE_BROWSER_EMULATION这个注册表项按 exe 名分别设置值渲染行为7000IE7 标准模式很多环境下的默认值8000IE8 标准模式10001IE10 标准模式11000IE11 边缘模式跟随文档 doctype11001IE11 强制标准模式推荐写在程序启动时必须在创建 WebBrowser 之前执行因为渲染模式是在内核初始化时读取的uses System.Win.Registry; procedure SetBrowserEmulation(const ExeName: string; Mode: Cardinal); var Reg: TRegistry; begin Reg : TRegistry.Create(KEY_WRITE); try Reg.RootKey : HKEY_CURRENT_USER; if Reg.OpenKey( Software\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_BROWSER_EMULATION, True) then begin Reg.WriteInteger(ExeName, Mode); // 键名是 exe 文件名不是完整路径 Reg.CloseKey; end; finally Reg.Free; end; end; // 调用位置dpr 里Application.Initialize 之前 SetBrowserEmulation(ExtractFileName(ParamStr(0)), 11001);键名一定要用ExtractFileName(ParamStr(0))只写文件名不带路径。写成完整路径的话注册表项能写进去但 IE 读不到表现是代码执行了但没效果。高 DPI 是配套的第二个问题。TWebBrowser不响应 per-monitor DPI 变化进程声明成 PerMonitorV2 之后跨屏拖动时网页内容会被位图拉伸得模糊。务实的选择是把进程声明成 system DPI aware让整个进程按主屏 DPI 渲染一次// dpr 里Application.Initialize 之前 SetProcessDPIAware;SetProcessDPIAware和 manifest 里写dpiAwaretrue/dpiAware效果等价代码方式胜在不用改资源文件。代价是跨屏拖动时不会重新缩放但至少不会糊成一片。5. 五个翻车现场皮肤不生效、白边、找不到单元的排查路径5.1 设计期一切正常编译出来是系统灰现象IDE 里跑得好好的皮肤齐全、标题栏漂亮一编译成 exe 拿到别的机器上整个界面退回 Windows 经典样式。原因皮肤文件没跟着发布。SkinsDirectory指向的目录在开发机上有.asz客户机上没有TsSkinManager加载失败后静默降级不报错也不弹框。解决发布清单里必须有Skins\目录并且里面的文件名和你 ini 里存的SkinName对得上。更稳的做法是加一层启动检查加载失败时写日志并回退到内置的默认皮肤。if not FileExists(SkinManager1.SkinsDirectory Name .asz) then begin // 记日志别静默吞掉 Log.Warn(皮肤文件缺失: Name); Name : Office2019White; end;5.2 组件面板上有图标一拖就报找不到单元现象面板里能看到TsSkinManager拖到窗体上编译器报Unit gsSkins not found或者Class TsSkinManager not found。原因只装了运行期包没装设计期包或者设计期包装了但Source目录没进 Library Path。前者的表现是面板上图标是灰的或者根本没有后者是面板正常但编译找不到源。解决先确认组件面板的页签是不是完整出现再检查 Tools → Options → Language → Delphi → Library 里的搜索路径。路径顺序也有讲究——如果列表里同时存在旧版本 AlphaControls 的路径且排在前面编译器会优先用旧的。把新路径上移到最前面然后做一次 Build All 而不是 Compile强制重编所有单元。5.3 最大化后标题栏多一条白边现象正常尺寸下完美一最大化标题栏顶部多出 1 到 8 像素不等的白条或者左右边框各缺一块。原因AllowExtBorders打开后非客户区尺寸由皮肤引擎算而 Windows 的 DWM 在最大化时会把窗口向外扩展一个边框宽度SM_CXSIZEFRAMESM_CXPADDEDBORDER。两边算法不一致差值就露出来了。解决三个方案按优先级排。一是统一 PPI 缩放比例让皮肤引擎和 VCL 用同一套 DPI前面 3.2 节给过代码。二是对最大化状态做特殊处理在WM_NCCALCSIZE里把多出来的边距吃掉。三是直接给主窗体关掉AllowExtBorders用系统标题栏只让内容区皮肤化——视觉上损失一点但绝不出错。交付期紧的时候我一般选第三个。5.4 换肤之后对话框、Hint、右键菜单还是系统样式现象主界面皮肤换得很彻底但弹出的确认框、输入框提示、表格的右键菜单还是 Windows 原生那套白底灰边。原因这些界面元素走的是不同的绘制路径。对话框由TsSkinManager.Options里的对话框开关控制Hint 由独立的 hint 组件控制右键菜单需要TsSkinProvider接管TPopupMenu。解决分三处配置。对话框在TsSkinManager.Options里打开对应开关Hint 需要放一个TsSkinHint类的组件并关联到 manager右键菜单则是在创建TPopupMenu之后把它的 owner 设成挂了TsSkinProvider的窗体。三处都配完界面才算是整体统一。这一条看着琐碎但客户验收时最先挑的就是这个弹框怎么还是老的。5.5 滚轮滚动时浏览器区域闪白底现象网页内容滚动时宿主窗口的背景色一闪而过尤其在内嵌到面板或者多标签页的时候特别明显。原因TWebBrowser的 ActiveX 子窗口重绘和 VCL 的双缓冲不同步。滚动时父窗口先擦背景子窗口还没来得及画中间那一帧就是白或者宿主窗口的Color属性值。解决把宿主容器的DoubleBuffered打开同时给容器设一个和网页背景一致的Color让空白帧看起来不像故障。另外ParentBackground也要设False否则会在皮肤背景上叠一层系统色。这些都是治标根治要靠TsWebBrowser自绘滚动条把重绘拉回 VCL 的消息链里。6. 把皮肤包压到 3MB 以内资源瘦身与上线前自检清单一个完整的 AlphaControls 皮肤包目录动辄二三十兆几十个.asz文件。全塞进安装包不是不行但客户端体积和首屏加载时间都会被拖累。我的做法是只留三到五个一个亮色主主题、一个暗色主题、一个高对比度给视力不好的用户剩下全部剔掉。剔之前先用前面 2.3 节的遍历脚本跑一遍看哪个皮肤在你的控件组合下没有渲染缺陷留下能用的那几个。瘦身还有一层.asz既可以作为外部文件随程序发布也可以编进资源里。外部文件的好处是换肤不用重新编译坏处是可能被用户误删编进资源反过来。我的折中是主主题编进资源当兜底其余放外部目录启动时优先读外部文件读不到就落到内置的。启动耗时这块实测下来皮肤激活本身不慢慢的是激活之后所有已存在窗体的重绘。如果主窗体上有几百个控件Active : True那一瞬间会有明显卡顿。可行的优化是先建控件、后激活皮肤// 主窗体 OnCreate 里先把界面搭完 BuildUI; // 再激活皮肤避免每个控件创建时都触发一次皮肤重绘 SkinManager1.Active : True;代价是启动瞬间会闪一下系统样式用一个 Splash 窗体盖住就行。上线前的自检我压成了一个小函数每次发版前跑一遍比人肉点界面靠谱// 返回空字符串表示通过否则是具体问题 function CheckSkinSetup(AForm: TCustomForm): string; begin if not Assigned(SkinManager1) then Exit(SkinManager 未创建); if not SkinManager1.Active then Exit(SkinManager 未激活); if not DirectoryExists(SkinManager1.SkinsDirectory) then Exit(皮肤目录不存在: SkinManager1.SkinsDirectory); if not FileExists(SkinManager1.SkinsDirectory SkinManager1.SkinName .asz) then Exit(当前皮肤文件缺失: SkinManager1.SkinName); if FindSkinProvider(AForm) nil then Exit(AForm.Name 缺少 TsSkinProvider); Result : ; end;这五项覆盖了绝大多数我这儿好好的、客户那儿不行的场景。之前有一次发版客户机上皮肤目录被安全软件清掉了程序启动是系统灰日志里什么都没有。从那以后我每次打包都强制走一遍这个检查并且在安装包里加一条Skins\*.asz的完整性校验少一个文件就拒绝启动并提示重新安装,总比让用户对着灰界面猜要好。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑