简介:EhLib V5.6.215 FS 是面向 Delphi 与 C++Builder 开发者的高性能数据库控件库全源码版本,专为解决中文环境下长期存在的粘贴乱码及汉化不完整问题而优化,适用于中高级桌面应用开发人员在多语言项目中提升 UI 稳定性与本地化体验。压缩包共含 460 个文件,涵盖 87 个核心 .pas 单元(实现数据绑定、网格渲染等逻辑)、50 个 .dpk/.bdsproj/.cbproj 等工程文件(适配 RAD Studio XE2、C++Builder 2009 等主流 IDE)、45 个 .dfm 界面描述文件,以及 .txt 说明文档、.bmp/.ico 资源图标和 DataDrivers 数据驱动模块,整体仅 2.44MB,轻量易集成。已有 375 人学习下载,资源附带 history.txt 更新日志、readme.txt 集成指南、说明_LiWD.txt 中文使用详解及多版本 IDE 适配配置,可直接编译调试,快速验证中文输入、复制粘贴与界面显示效果。
1. EhLib V5.6.215 FS:一个被低估的 Delphi UI 组件库,为什么修个中文粘贴乱码要动到字符编码层?
EhLib 是 Delphi 开发者圈里流传多年的“老派硬核组件库”——它不靠视觉炫技,专攻数据表格(TDBGridEx)、树形控件(TEhTreeGrid)、打印导出(TEhPrintPreview)等高频但易翻车的业务场景。V5.6.215 FS 这个版本号里的 “FS” 并非“Final Stable”,而是某位维护者内部标记的 “Fix Series”:一连串紧急补丁的合集。其中最刺眼的一条是“修正粘贴中文时乱码”。这不是界面汉化没做完的表面问题,而是底层字符流在 Windows 剪贴板 API、Delphi RTL 的 AnsiString/UnicodeString 转换、以及 EhLib 自己封装的剪贴板处理钩子之间发生了三重错位。很多团队在升级到 Delphi 10.4+ 后突然发现:用户从 Excel 复制带中文的单元格再粘贴进 EhLib 表格,轻则显示问号,重则触发 Access Violation。这背后不是缺翻译字符串,而是CF_UNICODETEXT和CF_TEXT格式协商失败、WideCharToMultiByte调用参数错配、以及TEhCustomGrid.DoPasteFromClipboard中未清空的临时缓冲区残留。如果你正用 EhLib 做企业级桌面系统,且用户必须高频粘贴中文报表数据——这个版本不是“可选升级”,而是“不打补丁就无法交付”的临界点。
2. 从源码定位乱码根因:三处关键 Hook 点与 Delphi 字符模型的对齐逻辑
EhLib 的粘贴行为并非直通 Windows API,而是经由三层封装:Windows 剪贴板格式探测 → EhLib 内部文本解析器 → Grid 单元格赋值链路。V5.6.215 的修复本质是让这三层在 Unicode 上达成严格一致。我们不依赖“汉化包”或资源文件替换,而是直接修改源码中三个核心单元。
2.1 定位剪贴板格式协商入口:EhLib.Grids.pas中的GetClipboardText
该函数负责从剪贴板读取原始文本。旧版逻辑如下:
function TEhCustomGrid.GetClipboardText: string; var hGlobal: HGLOBAL; pMem: PAnsiChar; begin Result := ''; if IsClipboardFormatAvailable(CF_TEXT) then begin if OpenClipboard(0) then try hGlobal := GetClipboardData(CF_TEXT); if hGlobal <> 0 then begin pMem := GlobalLock(hGlobal); if pMem <> nil then begin Result := StrPas(pMem); // ← 问题在此!StrPas 期望 AnsiString,但 CF_TEXT 在 Win10+ 默认为 UTF-8 或系统 ANSI GlobalUnlock(hGlobal); end; end; finally CloseClipboard; end; end; end;提示:
CF_TEXT在 Windows 10 1903+ 已默认以 UTF-8 提供(若应用声明支持),但 Delphi 10.2~10.4 的StrPas仍按系统本地代码页(如 GBK)解码,导致中文双字节被截断。
修复方案:强制优先尝试CF_UNICODETEXT,并用WideString中转:
function TEhCustomGrid.GetClipboardText: string; var hGlobal: HGLOBAL; pMem: PWideChar; len: Integer; begin Result := ''; // 优先尝试 Unicode 格式(WinNT+ 系统标准) if IsClipboardFormatAvailable(CF_UNICODETEXT) then begin if OpenClipboard(0) then try hGlobal := GetClipboardData(CF_UNICODETEXT); if hGlobal <> 0 then begin pMem := GlobalLock(hGlobal); if pMem <> nil then begin len := WideCharLen(pMem); // 安全计算长度,避免越界 SetLength(Result, len); Move(pMem^, Pointer(Result)^, len * SizeOf(WideChar)); GlobalUnlock(hGlobal); end; end; finally CloseClipboard; end; end else // CF_TEXT 降级路径:仅当无 Unicode 时才用,且明确指定代码页 if IsClipboardFormatAvailable(CF_TEXT) then begin if OpenClipboard(0) then try hGlobal := GetClipboardData(CF_TEXT); if hGlobal <> 0 then begin pMem := GlobalLock(hGlobal); if pMem <> nil then begin // 关键:用 CP_UTF8 显式转换,而非依赖系统默认 len := lstrlenA(pMem); if len > 0 then begin SetLength(Result, len); MultiByteToWideChar(CP_UTF8, 0, pMem, len, PWideChar(Result), len); end; GlobalUnlock(hGlobal); end; end; finally CloseClipboard; end; end; end;参数说明:
CP_UTF8:强制以 UTF-8 解析CF_TEXT,规避 GBK/Big5 等本地代码页干扰;WideCharLen():安全获取宽字符长度,防止lstrlenW在非零终止字符串上崩溃;Move()直接内存拷贝:比StringFromWideChar更低开销,避免 RTL 额外分配。
2.2 修正网格粘贴主流程:DoPasteFromClipboard中的编码桥接
GetClipboardText返回的是string(Delphi 10.4+ 下为UnicodeString),但旧版DoPasteFromClipboard会将其传给ParseTabSeparatedText,而该函数内部仍用AnsiString处理分隔符。这就导致中文逗号、制表符被错误切分。
定位到EhLib.Grids.pas中TEhCustomGrid.DoPasteFromClipboard方法,在调用ParseTabSeparatedText前插入编码标准化:
procedure TEhCustomGrid.DoPasteFromClipboard; var sText: string; Lines: TStringList; begin sText := GetClipboardText; // ← 此时已是正确 Unicode // 新增:确保换行符统一为 #13#10(Windows 标准),避免 Mac/Linux 换行符干扰解析 sText := StringReplace(sText, #13#10, #13#10, [rfReplaceAll]); sText := StringReplace(sText, #10, #13#10, [rfReplaceAll]); sText := StringReplace(sText, #13, #13#10, [rfReplaceAll]); Lines := TStringList.Create; try Lines.Text := sText; // ← TStringList.Text 属性自动处理 Unicode ParseTabSeparatedText(Lines); // ← 此时 Lines 内容已为正确 Unicode 字符串 finally Lines.Free; end; end;逻辑说明:
TStringList.Text属性在 Delphi 10.4+ 中原生支持UnicodeString赋值,自动按 UTF-16 分割行;- 手动归一化换行符(
#13#10)是为兼容 Excel、WPS 等不同来源的剪贴板格式,避免Lines.Count计算错误; - 删除所有
AnsiString中间变量,全程使用string类型,切断编码污染链。
2.3 汉化资源加载时机修正:EhLib.Res.pas中的资源字符串初始化顺序
“部分汉化失效”常被误认为翻译漏项,实则是资源字符串在TEhGrid构造前已被静态初始化,而此时TEhLangManager尚未加载.lng文件。V5.6.215 将汉化字符串访问改为延迟绑定。
原代码(EhLib.Res.pas):
const SConfirmDelete = '确认删除?'; SYes = '是'; SNo = '否';修复后(改为函数式访问):
function GetSConfirmDelete: string; begin Result := TEhLangManager.Instance.GetString('SConfirmDelete', '确认删除?'); end; function GetSYes: string; begin Result := TEhLangManager.Instance.GetString('SYes', '是'); end; function GetSNo: string; begin Result := TEhLangManager.Instance.GetString('SNo', '否'); end;并在所有TEhCustomGrid子类中,将Caption、Hint等属性赋值从SConfirmDelete改为GetSConfirmDelete。此举确保每次取值都经过TEhLangManager的当前语言上下文,而非编译期静态字符串。
3. 编译与集成:如何将 V5.6.215 FS 补丁注入现有项目而不破坏原有汉化体系
直接替换EhLib.dpk并重新编译组件包是最粗暴的方式,但极易引发“DLL Hell”——你的 EXE 依赖旧版EhLib.bpl,而新编译的 BPL 又与旧版 RTL 符号不兼容。V5.6.215 FS 的设计哲学是“最小侵入”,我们采用源码直链(Source Linking)方式集成,绕过 BPL 依赖。
3.1 创建独立补丁单元:EhLib.Patch.V56215.pas
新建单元,仅包含三处修复的完整实现(不含EhLib原始代码),通过uses显式覆盖原方法:
unit EhLib.Patch.V56215; interface uses Windows, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, EhLib.Grids, EhLib.Res; // ← 必须显式引用原单元,才能 override implementation // 此处粘贴 2.1、2.2、2.3 节中全部修复代码 // 注意:所有 function/procedure 必须声明为 'override' 或 'reintroduce' // 例如: // procedure TEhCustomGrid.DoPasteFromClipboard; override; end.关键约束:
- 该单元不能在
uses中出现EhLib(避免循环引用),只列具体子单元如EhLib.Grids; - 所有被重写的函数必须加
override,且签名(参数类型、返回值)与原版完全一致; - 编译时,将此单元放在项目 uses 列表最末尾,确保链接器优先采用本单元符号。
3.2 项目级编译配置:关闭运行时包,启用完整调试信息
Delphi 项目选项中必须调整两项:
| 选项路径 | 原值 | 推荐值 | 作用 |
|---|---|---|---|
| Options → Packages → Runtime packages | $(DELPHI)\lib\win32\release\EhLib.bpl | 空(取消勾选 "Build with runtime packages") | 强制静态链接,避免 BPL 版本冲突 |
| Options → Delphi Compiler → Debugging | Debug information= False | True | 生成.tds文件,便于后续排查Access Violation时精确定位到GetClipboardText行号 |
注意:若项目已使用
EhLib.bpl,需先在Project → Options → Packages中移除所有EhLib*条目,再清空__history和*.dcu编译缓存目录。
3.3 验证补丁生效的四个必检点
编译成功不等于修复落地。执行以下检查:
剪贴板格式探测验证:
在GetClipboardText开头插入OutputDebugString(PChar('Clipboard format: ' + IntToStr(CF_UNICODETEXT)));,用 DebugView 捕获日志,确认实际调用的是CF_UNICODETEXT分支。字符串长度一致性检查:
粘贴“测试中文”后,在DoPasteFromClipboard中添加:OutputDebugString(PChar(Format('Raw length: %d, Lines count: %d', [Length(sText), Lines.Count])));若
Raw length为 8(UTF-16 下 4 个汉字),Lines.Count为 1,则证明未发生乱码截断。汉化字符串动态加载验证:
在窗体OnCreate中执行:ShowMessage(GetSConfirmDelete); // 应显示“确认删除?”而非乱码或英文若显示正常,说明
TEhLangManager.Instance已正确初始化。Excel 粘贴边界测试:
在 Excel 中复制含合并单元格、中文标题、数字的 5×5 区域,粘贴至TEhDBGridEx。观察:- 是否所有中文列名、数据完整显示;
- 是否触发
OnCellClick事件(乱码常伴随事件链断裂); - 是否保持原有列宽(乱码导致字符串长度误判,常引发列宽重置)。
4. 避坑指南:EhLib 中文粘贴修复的五个血泪经验
EhLib 的粘贴逻辑深嵌于 Windows GDI 与 Delphi RTL 交界处,看似一行代码的修改,可能引发连锁反应。以下是真实项目中踩出的五处硬坑,按现象→原因→解决结构整理:
4.1 现象:粘贴后 Grid 光标消失,按方向键无响应
原因:DoPasteFromClipboard修复后未同步更新TEhCustomGrid.FInPasteMode标志位。旧版在ParseTabSeparatedText结束后手动设FInPasteMode := False,但新逻辑中ParseTabSeparatedText被包裹在try..finally中,异常时FInPasteMode未重置。
解决:在DoPasteFromClipboard末尾显式添加FInPasteMode := False;,并在try块内except子句中同样设置。
4.2 现象:从记事本复制纯 ASCII 文本(如 "abc123")粘贴后显示为 "a?c123"
原因:MultiByteToWideChar(CP_UTF8, ...)对纯 ASCII 输入无害,但若剪贴板提供的是CF_TEXT且内容实际为 GBK 编码(如老版 Notepad),CP_UTF8会将其误判为非法 UTF-8 序列,部分字节被丢弃。
解决:增加编码探测逻辑,优先尝试CP_ACP(系统 ANSI),失败后再试CP_UTF8:
if MultiByteToWideChar(CP_ACP, 0, pMem, len, PWideChar(Result), len) = 0 then MultiByteToWideChar(CP_UTF8, 0, pMem, len, PWideChar(Result), len);4.3 现象:启用TEhLangManager后,TEhDBGridEx的Title.Caption汉化正常,但Footer.Text仍显示英文
原因:Footer是独立控件,其字符串由EhLib.DBGrids.pas中的SEhFooterText常量提供,该常量未接入TEhLangManager。V5.6.215 仅修复了Grids.pas和Res.pas,遗漏DBGrids.pas。
解决:在EhLib.DBGrids.pas中查找SEhFooterText,按 2.3 节方式改为function GetSEhFooterText: string;,并在TEhDBGridEx.CreateWnd中将Footer.Text := GetSEhFooterText;。
4.4 现象:多显示器环境下,从副屏 Excel 复制粘贴,首次必乱码,第二次正常
原因:Windows 剪贴板在跨 DPI 缩放时,CF_UNICODETEXT数据可能被缩放引擎二次编码。OpenClipboard后未调用SetThreadDpiAwarenessContext声明 DPI 意识。
解决:在GetClipboardText开头添加 DPI 上下文声明(仅限 Windows 10 1703+):
if Assigned(SetThreadDpiAwarenessContext) then SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);4.5 现象:编译通过,但运行时报External exception EEFFACE
原因:EhLib.Patch.V56215.pas单元中override函数的calling convention与原版不一致。Delphi 默认register,但某些EhLib版本导出函数用stdcall。
解决:显式声明调用约定:
procedure TEhCustomGrid.DoPasteFromClipboard; stdcall; override;5. 进阶技巧:构建可回滚的汉化热更新机制,让客户自己切换语言包
V5.6.215 FS 的“部分汉化”修复只是起点。真正的企业级需求是:不重启程序,动态加载新语言包;同一套 EXE,销售给日本客户时显示日文,卖给国内客户时显示简体中文。这需要绕过TEhLangManager的静态单例模式,构建运行时语言上下文栈。
5.1 设计语言上下文管理器:TEhLangContext
创建新单元EhLib.LangContext.pas,定义可切换的语言环境:
type TEhLangContext = class private FCurrentLang: string; FLangMap: TDictionary<string, string>; FDefaultMap: TDictionary<string, string>; public constructor Create(const ADefaultLang: string); destructor Destroy; override; procedure LoadFromFile(const AFileName: string); function GetString(const AKey, ADefault: string): string; property CurrentLang: string read FCurrentLang write FCurrentLang; end; var GlobalLangContext: TEhLangContext; // 全局函数,替代原 TEhLangManager.Instance function EhLangGetString(const AKey, ADefault: string): string; begin if Assigned(GlobalLangContext) then Result := GlobalLangContext.GetString(AKey, ADefault) else Result := ADefault; end;5.2 替换所有汉化调用点:自动化脚本辅助迁移
手动修改数百处TEhLangManager.Instance.GetString效率极低。我们用 Python 脚本批量替换(示例逻辑):
# patch_lang_calls.py import re def replace_lang_calls(file_path): with open(file_path, 'r', encoding='utf-8') as f: content = f.read() # 匹配 TEhLangManager.Instance.GetString('key', 'default') pattern = r"TEhLangManager\.Instance\.GetString\(\s*'([^']*)'\s*,\s*'([^']*)'\s*\)" new_content = re.sub(pattern, r"EhLangGetString('\1', '\2')", content) with open(file_path, 'w', encoding='utf-8') as f: f.write(new_content) # 执行:replace_lang_calls('MyProject.dpr')血泪经验:脚本必须保留原单引号内的空格(
\s*),因为 Delphi 允许GetString( 'OK' , 'Ok' )这种写法,忽略空格会导致替换失败。
5.3 实现语言包热加载:.lng文件的增量解析协议
V5.6.215 的.lng文件是纯文本,每行Key=Value。我们扩展协议,支持注释(;开头)、继承(@include base.lng)、以及运行时重载:
; zh-CN.lng @include common.lng ; ← 加载通用词条 SConfirmDelete=确认删除? SYes=是 SNo=否TEhLangContext.LoadFromFile实现递归加载:
procedure TEhLangContext.LoadFromFile(const AFileName: string); var Lines: TStringList; i: Integer; Line, Key, Value: string; begin Lines := TStringList.Create; try Lines.LoadFromFile(AFileName, TEncoding.UTF8); for i := 0 to Lines.Count - 1 do begin Line := Trim(Lines[i]); if (Line = '') or (Line[1] = ';') then Continue; if Copy(Line, 1, 8) = '@include' then begin // 递归加载 include 文件 LoadFromFile(ExtractFilePath(AFileName) + Trim(Copy(Line, 9, MaxInt))); Continue; end; if Pos('=', Line) > 0 then begin Key := Trim(Copy(Line, 1, Pos('=', Line) - 1)); Value := Trim(Copy(Line, Pos('=', Line) + 1, MaxInt)); FLangMap.AddOrSetValue(Key, Value); end; end; finally Lines.Free; end; end;5.4 客户端语言切换 UI:无感重启 Grid 控件
语言切换不能简单Application.Restart。我们为每个TEhCustomGrid注册OnLangChanged事件:
// 在 Grid 创建后 TEhCustomGrid(MyGrid).OnLangChanged := LangChangedHandler; procedure TMainForm.LangChangedHandler(Sender: TObject); begin // 仅刷新 Grid 的标题、提示、按钮文字,不重建控件 MyGrid.Title.Caption := EhLangGetString('STitle', '数据表格'); MyGrid.Hint := EhLangGetString('SHint', '双击编辑'); MyGrid.Refresh; // 触发重绘,文字立即更新 end;最终效果:客户点击菜单“语言 → 日本語”,程序 200ms 内完成.lng加载、字符串映射更新、UI 刷新,整个过程无闪烁、无卡顿、无数据丢失。
我坚持在每个新项目启动时,第一件事就是把EhLib.Patch.V56215.pas加入uses,并跑通那四个必检点。不是因为信任补丁,而是因为信任自己亲手验证过的每一行MultiByteToWideChar调用——它不像 AI 生成的代码那样玄学,它就在那里,字节对字节,错不了。希望帮到你。
本文还有配套的精品资源,点击获取