- UID
- 4293
- 精华
- 积分
- 11233
- 威望
- 点
- 宅币
- 个
- 贡献
- 次
- 宅之契约
- 份
- 最后登录
- 1970-1-1
- 在线时间
- 小时
|
本帖最后由 YY菌 于 2024-12-11 15:26 编辑
原帖来源:【Win32API】资源加载Ex版(国际化支持的扩展、资源API的内部实现原理研究)https://www.0xaa55.com/thread-26602-1-1.html(出处: 技术宅的结界)
众所周知,Win32的PE文件内嵌资源有标准资源类型和自定义资源类型两大类,标准资源类型又分为 光标、位图、图标、菜单、对话框、字符串、字体目录、字体、加速键等,根据教程或官方文档等常见的资料,要加载它们分别需要使用以下API:
资源 | API | 光标 | LoadCursor | 位图 | LoadBitmap | 图标 | LoadIcon | 菜单 | LoadMenu | 对话框 | CreateDialog | DialogBox | 字符串 | LoadString | 字体目录 | 无 | 字体 | 无 | 加速键 | LoadAccelerators | 自定义资源 | FindResource + LoadResource |
最开始我们根据教程或官方文档来使用,认为确实应该如此,并没有想过其中的原理,更没有想过标准资源和自定义资源只是在应用上差别,还是有着本质上区别?随着后来的深入,开始有了国际化方向的编程需求,这时候一般大家都会发现在VS的资源编辑器中可以设置资源的语言,相同的类型和ID可以存放多个不同语言的资源,于是便会想到在Win32编程中设计国际化UI最简单方便的方案便是直接利用Win32资源的语言功能。这个时候又会考虑的新的问题,如何加载多语言的资源?平常我们加载资源都是直接使用API按类型和ID来加载没有考虑过语言相关的问题,经过测试后可以发现如果存在多个语言的资源,系统会根据用户安装的操作系统语言来加载对应的资源(英文系统上加载英文资源,中文简体系统加载中文简体资源,中文繁体系统加载中文繁体资源)符合大部分情况下的需要,但要是想专门加载特定语言资源怎么办呢?查一下相关的API就会发现自定义资源有个叫 FindResourceEx 的扩展API,但是加载标准资源的API居然没有Ex版(是不是非常想打比尔盖茨等人打一顿)。
后来随着技术的更进一步深入,又发现了 RT_ 开头的常量宏,一看名称这不就是哪些所谓的标准资源类型吗?这也就意味着标准资源类型和自定义资源类型根本就没有任何本质区别,唯一的区别就只是标准资源类型都使用的整数来作为类型编号,而自定义资源类型则是用字符串来作为类型名称的。这个时候终于恍然大悟了,资源ID都有整数编号和字符串名称两种方式,裆燃资源的类型也有啊。但是新的问题又出现了,以前用的哪些标准资源类型加载的API都是返回已经创建好的对象了,自己用 FindResourceEx + LoadResource 加载出现的只是原始数据,并不能直接拿来就用。于是继续深入研究,搞懂了大部分标准资源加载API的实现原理,于是遍封装了一套Ex版的标准资源加载函数:
资源 | API | 菜单 | LoadMenuEx | 对话框 | CreateDialogEx | DialogBoxEx | 字符串 | LoadStringEx | 加速键 | LoadAcceleratorsEx |
实现原理:
API | 实现ID | 备注 | CreateDialogEx | FindResourceEx + LoadResource + CreateDialogIndirectParam | 加载资源为非模态对话框 | DialogBoxEx | FindResourceEx + LoadResource + DialogBoxIndirectParam | 加载资源为模态对话框 | LoadMenuEx | FindResourceEx + LoadResource + LoadMenuIndirect | 加载菜单资源 | LoadAcceleratorsEx | FindResourceEx + LoadResource + CreateAcceleratorTableW | 加载加速键资源(提示PE资源中保存的加速键始终是Unicode数据,所以不管是LoadAcceleratorsExA还是LoadAcceleratorsExW加速键句柄都必须用CreateAcceleratorTableW创建,唯一区别只是查找资源时用FindResourceExA还是FindResourceExW) | LoadStringExA | FindResourceEx + LoadResource + WideCharToMultiByte | 加载字符串资源(由于资源中始终是Unicode文本,因此需要用WideCharToMultiByte转换字符串编码) | LoadStringExW | FindResourceEx + LoadResource + CopyMemory | 加载字符串资源(由于资源中始终是Unicode文本,因此直接拷贝内存到缓冲区并添加\0结束标记即可) |
菜单、对话框、加速键都是在原来的基础上增加一个语言参数(就和 FindResource 跟 FindResourceEx 一样),加载字符串资源的 LoadStringEx 函数则是稍微有一些调整,增加的是更高级一些 LCID 参数,这个操作主要是针对 LoadStringExA 设计考虑的,可以想想一下只是简简单单的切换一下资源语言,而不是切换 ANSI 对应的字符集会大概率产生乱码?LCID 的低16位就是LANGID,而 GetLocaleInfo 可以获取 LCID 对应的字符集代码页,因此使用 LCID 更好的解决 LoadStringExA 的问题。至于 LoadStringExW 嘛为了保证参数的一致性以及对通用字符集(TCHAR系列)的兼容,那自然也使用 LCID 来作为扩展参数,只是W版就只使用低16位的语言ID,高位全部忽略。
标准的 LCID 只使用了20bit,算上 VERSION 字段总共最多也就24bit,还有8bit空闲区域,于是我就把这块区域用来做一些扩展标记。用法如下:
- const LCID lcid = MAKESORTLCID(MAKELANGID(LANG_主语言, SUBLANG_主语言_子语言), 分类ID, 版本); // 将要加载的字符串语言和区域信息
- const int 缓冲区长度 = …; // 注意:需包括\0结束标记
- int 字符串长度; // 提示:不包括\0结束标记
- CHAR ANSI字符串缓冲区[缓冲区长度];
- WCHAR WIDE字符串缓冲区[缓冲区长度];
- TCHAR AUTO字符串缓冲区[缓冲区长度];
- LPCWSTR 原始字符串地址;
- // 加载不同语言和不同字符集的处理(此方案仅适用于 LoadStringExA)
- 字符串长度 = LoadStringExA(hInstance, IDS_字符串ID, lcid, ANSI字符串缓冲区, 缓冲区长度); // 加载指定语言的字符串资(使用当前线程的ACP)
- 字符串长度 = LoadStringExA(hInstance, IDS_字符串ID, LOCALE_USE_CP_LCID | lcid, ANSI字符串缓冲区, 缓冲区长度); // 加载指定语言的字符串资(使用LCID对应的CP,简体中文是GB2312,繁体中文是BIG-5)
- 字符串长度 = LoadStringExA(hInstance, IDS_字符串ID, LOCALE_USE_CP_UTF8 | lcid, ANSI字符串缓冲区, 缓冲区长度); // 加载指定语言的字符串资(使用Unicode的CP UTF-8)
- // 加载Unicode文本(此方案仅适用于 LoadStringExW,dwLocaleCP 只使用LANGID部分)
- 字符串长度 = LoadStringExW(hInstance, IDS_字符串ID, lcid, WIDE字符串缓冲区, 缓冲区长度); // 加载指定语言的字符串资源到WIDE缓冲区(加载完成会自动补充\0结束标记)
- 字符串长度 = LoadStringExW(hInstance, IDS_字符串ID, lcid, LPWSTR(原始字符串地址), 0); // 获取指定语言的字符串资源的原始数据(不包括\0结束标记,需自己手动根据返回的字符串长度来处理)
- // 新增功能(LoadString不支持)A和W均适用(ANSI返回字节长度,WIDE返回字符个数)
- 字符串长度 = LoadStringEx(hInstance, IDS_字符串ID, lcid, nullptr, 0); // 仅获取指定语言的文本长度,不加载文本内容(可用于计算所需缓冲区大小,需注意缓存区分配时的长度要在此基础上 + 1)
- 字符串长度 = LoadStringEx(nullptr, 对话框按钮ID, lcid, AUTO字符串缓冲区, 缓冲区长度); // 获取指定语言的对话框按钮文本(IDOK为"确定"、IDCANCEL为"取消"、…),模仿 LoadImage 在使用空 hInstance 时的加载系统预设图像的功能,LoadStringEx 对应的也就是加载系统预设文本的功能。
复制代码
语言名称 | 语言ID | 代码页 | 备注 | 非特定语言(系统用户语言) | MAKELANGID(LANG_NEUTRAL, SUBLANG_NEUTRAL) | 0 | 加载无语言类型的资源(如果不存在则会依次查找线程、用户、系统、英语(美国)的资源) | 英语(美国) | MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US) | 1252 | 中文简体(中国大陆) | MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED) | 936 | 中文繁体(台湾) | MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_TRADITIONAL) | 950 | 俄语(俄罗斯) | MAKELANGID(LANG_RUSSIAN, SUBLANG_RUSSIAN_RUSSIA) | 1251 | 英语(英国) | MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_UK) | 1252 | 法语(法国) | MAKELANGID(LANG_FRENCH, SUBLANG_FRENCH) | 1252 | 中文简体(新加坡) | MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_SINGAPORE) | 936 | 中文繁体(香港) | MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_HONGKONG) | 950 | 中文繁体(澳门) | MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_MACAU) | 950 | 日语(日本) | MAKELANGID(LANG_JAPANESE, SUBLANG_JAPANESE_JAPAN) | 932 |
LCID组合参数 | 备注 | LOCALE_USE_CP_THREAD | 使用当前线程ANSI代码页(默认) | LOCALE_USE_CP_ACP | 使用系统用户ANSI代码页 | LOCALE_USE_CP_OEMCP | 使用 OEM 代码页 | LOCALE_USE_CP_MACCP | 使用 Macintosh 代码页 | LOCALE_USE_CP_UTF7 | 使用 UTF-7 代码页 | LOCALE_USE_CP_UTF8 | 使用 UTF-8 代码页 | LOCALE_USE_CP_LCID | 使用 LCID 语言对应的代码页 |
对话框按钮ID | 英文 | 中文简体 | 中文繁体 | 日语 | 俄语 | 备注 | 0 | Error | 错误 | 錯誤 | エラー | Ошибка | 消息框默认标题 | IDOK | 1 | OK | 确定 | 確定 | OK | ОК | 确定按钮文本 | IDCANCEL | 2 | Cancel | 取消 | 取消 | キャンセル | Отмена | 取消按钮文本 | IDABORT | 3 | &Abort | 中止(&A) | 中止(&A) | 中止(&A) | Пр&ервать | 中止按钮文本 | IDRETRY | 4 | &Retry | 重试(&R) | 重試(&R) | 再試行(&R) | По&втор | 重试按钮文本 | IDIGNORE | 5 | &Ignore | 忽略(&I) | 略過(&I) | 無視(&I) | &Пропустить | 忽略按钮文本 | IDYES | 6 | &Yes | 是(&Y) | 是(&Y) | はい(&Y) | &Да | 是按钮文本 | IDNO | 7 | &No | 否(&N) | 否(&N) | いいえ(&N) | &Нет | 否按钮文本 | IDCLOSE | 8 | &Close | 关闭(&C) | 關閉(&C) | 閉じる(&C) | &Закрыть | 关闭按钮文本 | IDHELP | 9 | Help | 帮助 | 說明 | ヘルプ | Справка | 帮助按钮文本 | IDTRYAGAIN | 10 | &Try Again | 重试(&T) | 重試(&T) | 再実行(&T) | &Повторить | 另一种重试按钮文本 | IDCONTINUE | 11 | &Continue | 继续(&C) | 繼續(&C) | 続行(&C) | &Продолжить | 继续按钮文本 |
测试
|
|