Windows菜单本地化方法
本文所介绍的内容不尽是菜单本地化,也包括窗口和控件等的本地化方法。所谓本地化,就是将程序的界面文字设置为使用者所要求的语言环境。如一个软件不需要修改源代码,而是通过本地化操作,使软件界面能显示简体中文、繁体中文、英语等某一指定的文字。
本地化的实质就是软件资源更改,Windows的API函数提供了这个功能。涉及到本地化的资源,主要有菜单、消息表、版本信息和字符串表。消息表的创建与本地化在《使用FormatMessage函数实现多语言消息》中已经介绍过了,这里不再重述。
其实只要实现版本信息和字符串表本地化,就可以对软件界面进行本地化,这样问题就会变得简单。但为了介绍软件本地化的基本方法,有必要对菜单资源本地化方法进行介绍,因为这是所有资源本地化的基本方法。
1. 多语言资源的承载
要对一个软件进行本地化,就需要有一个包含多语言资源的承载文件。例如我们的编写的软件可以在简体中文、繁体中文、英语环境中使用,那就需要有一个承载文件,这个文件必须同时包含了简体中文、繁体中文、英语的资源。能承载多语言资源文件有三种文件类型,即DLL文件、EXE文件、MUI文件。MUI文件(.mui)需要专用工具Muirct.exe来创建,并需要LoadMUILibrary函数来装载,有些计算机中没有Muirct.exe工具,也没有LoadMUILibrary函数,所以不介绍该承载文件。
DLL文件、EXE文件来承载多语言资源与编写普通的DLL文件、EXE文件的过程和方法是相同的。参照Win10系统软件的做法,如果是用于本地化DLL文件的,就创建一个承载多语言资源的DLL文件;如果是用于本地化EXE文件的,就创建一个承载多语言资源的EXE文件。
举例来说,如果我们编写了一个可以发布的Test.dll动态库文件,那么就创建一个承载多语言资源的DLL文件,并将文件名改为Test.dll.mui;如果我们编写了一个可以发布的Test.exe软件,那么就创建一个承载多语言资源的EXE文件,并将文件名改为Test.exe.mui。这种约定一目了然,便于管理,这也是Win10系统软件的本地化的管理方法。
注意,上述的Test.dll.mui和Test.exe.mui文件,并不是".mui"文件,是由LoadLibraryEx函数装载的普通DLL和EXE文件,只是改变文件名而已,不需要用LoadMUILibrary函数来装载。
因为DLL文件和EXE文件对于承载资源的方法是相同的,所以下面以EXE文件为例进行介绍。
2. 菜单本地化实例
如果我们编写了一个可以发布的Test.exe软件,软件可以在简体中文、繁体中文、英语环境中使用。因为软件中使用了菜单,所以需要对菜单进行本地化处理。
2.1 编写多语言的菜单资源
因为软件本地化时,一般都会对软件的版本信息进行本地化,所以资源中一般包含版本资源。
先写一个资源头文件,文件名指定为"mui.h",内容如下:
#define MENU_ID 10 //主菜单名标识
#define IDM_FILE 100
#define IDM_NEW 101
#define IDM_OPEN 102
#define IDM_SAVE 103
#define IDM_SAVEAS 104
#define IDM_CLOSE 105
#define IDM_EDIT 200
#define IDM_UNDO 201
#define IDM_CUT 202
#define IDM_COPY 203
#define IDM_PASTE 204
#define IDM_DEL 205
#define IDM_HELP 300
#define IDM_ABOUT 301
再写3个菜单资源文件,文件名分别为"rsrc_0404.rc"、“rsrc_0409.rc”、“rsrc_0804.rc”。文件名中的数字为语言标识的十六进制值(可以查阅相关的Windows文档获取),0404h为繁体中文,0409h为英语、0804h为简体中文。
有关版本资源的语法可以查阅Windows文档。
rsrc_0404.rc文件内容如下:
//==================================
// 繁体中文
//==================================
LANGUAGE 4,1 //0x404
//=================================
// 版本信息
//=================================
VS_VERSION_INFO VERSIONINFO
FILEVERSION 1, 0, 0, 0
PRODUCTVERSION 1, 0, 0, 0
FILEOS VOS__WINDOWS32
FILETYPE VFT_APP
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040404B0"
BEGIN
VALUE "CompanyName", "The MASM32 SDK\000"
VALUE "FileDescription", "Default Template\000"
VALUE "FileVersion", "1.0\000"
VALUE "InternalName", "template\000"
VALUE "OriginalFilename", "template.exe\000"
VALUE "LegalCopyright", "\251 2016 The MASM32 SDK\000"
VALUE "ProductName", "template\000"
VALUE "ProductVersion", "1.0\000"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x404, 0x4B0
END
END
//菜单定义
MENU_ID MENUEX
BEGIN
POPUP "文 件",IDM_FILE
BEGIN
MENUITEM "新 建",IDM_NEW
MENUITEM "打 開",IDM_OPEN
MENUITEM "保 存",IDM_SAVE
MENUITEM "另存為",IDM_SAVEAS
MENUITEM "關 閉",IDM_CLOSE
END
POPUP "編 輯",IDM_EDIT
BEGIN
MENUITEM "取 消",IDM_UNDO
MENUITEM "剪 切",IDM_CUT
MENUITEM "復 製",IDM_COPY
MENUITEM "粘 貼",IDM_PASTE
MENUITEM "刪 除",IDM_DEL
END
POPUP "幫 助",IDM_HELP
BEGIN
MENUITEM "關 於",IDM_ABOUT
END
END
rsrc_0409.rc文件内容如下:
//=================================
// 美国英语
//=================================
LANGUAGE 9,1 //0x409
//=================================
// 版本信息
//=================================
VS_VERSION_INFO VERSIONINFO
FILEVERSION 1, 0, 0, 0
PRODUCTVERSION 1, 0, 0, 0
FILEOS VOS__WINDOWS32
FILETYPE VFT_APP
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904B0"
BEGIN
VALUE "CompanyName", "The MASM32 SDK\000"
VALUE "FileDescription", "Default Template\000"
VALUE "FileVersion", "1.0\000"
VALUE "InternalName", "template\000"
VALUE "OriginalFilename", "template.exe\000"
VALUE "LegalCopyright", "\251 2016 The MASM32 SDK\000"
VALUE "ProductName", "template\000"
VALUE "ProductVersion", "1.0\000"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 0x4B0
END
END
//菜单定义
MENU_ID MENUEX
BEGIN
POPUP "File",IDM_FILE
BEGIN
MENUITEM "New",IDM_NEW
MENUITEM "Open",IDM_OPEN
MENUITEM "Save",IDM_SAVE
MENUITEM "SaveAs",IDM_SAVEAS
MENUITEM "Close",IDM_CLOSE
END
POPUP "Edit",IDM_EDIT
BEGIN
MENUITEM "Undo",IDM_UNDO
MENUITEM "Cut",IDM_CUT
MENUITEM "Copy",IDM_COPY
MENUITEM "Paste",IDM_PASTE
MENUITEM "Delete",IDM_DEL
END
POPUP "Help",IDM_HELP
BEGIN
MENUITEM "About",IDM_ABOUT
END
END
rsrc_0804.rc文件内容如下:
//=================================
// 简体中文
//=================================
LANGUAGE 4,2 //0x804
//=================================
// 版本信息
//=================================
VS_VERSION_INFO VERSIONINFO
FILEVERSION 1, 0, 0, 0
PRODUCTVERSION 1, 0, 0, 0
FILEOS VOS__WINDOWS32
FILETYPE VFT_APP
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "080404B0"
BEGIN
VALUE "CompanyName", "The MASM32 SDK\000"
VALUE "FileDescription", "Default Template\000"
VALUE "FileVersion", "1.0\000"
VALUE "InternalName", "template\000"
VALUE "OriginalFilename", "template.exe\000"
VALUE "LegalCopyright", "\251 2016 The MASM32 SDK\000"
VALUE "ProductName", "template\000"
VALUE "ProductVersion", "1.0\000"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x804, 0x4B0
END
END
//菜单定义
MENU_ID MENUEX
BEGIN
POPUP "文 件",IDM_FILE
BEGIN
MENUITEM "新 建",IDM_NEW
MENUITEM "打 开",IDM_OPEN
MENUITEM "保 存",IDM_SAVE
MENUITEM "另存为",IDM_SAVEAS
MENUITEM "关 闭",IDM_CLOSE
END
POPUP "編 辑",IDM_EDIT
BEGIN
MENUITEM "取 消",IDM_UNDO
MENUITEM "剪 切",IDM_CUT
MENUITEM "复 制",IDM_COPY
MENUITEM "粘 粘",IDM_PASTE
MENUITEM "刪 除",IDM_DEL
END
POPUP "帮 助",IDM_HELP
BEGIN
MENUITEM "关 于",IDM_ABOUT
END
END
然后写一个包含全部语言的资源文件,文件名指定为"mui.rc",内容如下:
#include "\masm32\include64\resource.h"
#include <mui.h>
rcinclude Rsrc_0404.rc
rcinclude Rsrc_0804.rc
rcinclude Rsrc_0409.rc
2.2 创建一个承载多语言资源的EXE文件
这个文件源码很简单,因为只是为了承载资源。以下为Masm64源码,源码文件名指定的mui.asm。
.code
entry_point proc
rcall ExitProcess,0
ret
entry_point endp
end
然后编译这个程序。在Masm64编程环境中,我们会创建一个批处理文件来编译,如下所示:
@echo off
\masm32\bin64\rc.exe mui.rc
\masm32\bin64\ml64.exe /c /nologo Mui.asm
\masm32\bin64\link.exe /SUBSYSTEM:WINDOWS /ENTRY:entry_point /LARGEADDRESSAWARE Mui.obj mui.res /OUT:Test.exe.mui
pause
注意,上述的link.exe选项中增加一个"/OUT:Test.exe.mui"项目,使其输出文件名为"Test.exe.mui",而不能"mui.exe",如前面所述,其实质还是EXE文件。你也可以不用这个选项,编译成功后再将"mui.exe"文件名改为"Test.exe.mui"。
2.3 本地化操作
已经创建了一个包含简体中文、繁体中文和英语的"Test.exe.mui"文件,现在就可以对Test.exe软件进行本地化操作。一般情况我们会写一个通用的本地化程序,将需要本地化软件名和用于本地化的多语言资源承载文件名等,通过命令行参数传递给通用的本地化程来实现操作,这样就不需要为某个程序的本地化特别地去编写一个程序。
本地化操作的两个主要函数代码如下:
;==========================================================
;更新指定的资源
;入: hUpdate=目标文件的更新句柄(BeginUpdateResource函数返回)
; rType=要更新的资源类型
; wId=要更新的资源ID
; wlang=要更新的资源语言ID(一般为0)
; hInst=源资源模块句柄
; uId=源模块资源中用于更新的资源ID
; ulang=源模块资源中用于更新的资源语言ID
;出: RAX=TRUE: 成功
; =FALSE: 失败
;-------------------------------------------------------
;对于字符串资源,wId和uId为字符串组号(1...),也就是说
;字符串以组为单位更新,每组16个字符串。
;=========================================================
Rsrc_Update proc hUpdate:QWORD,rType:QWORD,wId:QWORD,\
wlang:QWORD,hInst:QWORD,uId:QWORD,ulang:QWORD
LOCAL ss_size:DWORD
LOCAL ss_hRes:QWORD
;---在源资源模块中搜索资源---
invoke FindResourceEx,hInst,rType,uId,ulang ;搜索资源
test rax,rax
jz ss_0
mov ss_hRes,rax
invoke SizeofResource,hInst,ss_hRes ;获取资源字节长度
mov ss_size,eax
invoke LoadResource,hInst, ss_hRes ;装入该资源
test rax,rax
jz ss_0
invoke LockResource,rax ;获取资源内存地址
test rax,rax
jz ss_0
;---将资源复制到目标文件---
mov r10,rax
invoke UpdateResource,hUpdate,rType,wId,wlang,r10,ss_size
ss_0:
ret
Rsrc_Update endp
;======================================================
;本地化操作主函数
;入: pExeFile=需要本地化的可执行文件的完整路径文件名
; Unicode串
; Id=要更新的菜单名标识ID
; pMuiFile=包含多语言资源的EXE文件(或DLL)的完整路径文件名
; Unicode串
; ulang=要使用的资源语言ID。例:
; 404h(1028)--繁体中文。
; 409h(1033)--美国英语。
; 804h(2052)--简体中文。
;======================================================
Localize proc pExeFile:QWORD,Id:QWORD,\
pMuiFile:QWORD,ulang:QWORD
LOCAL ss_hInst:QWORD
LOCAL ss_hUpdate:QWORD
cmp rcx,r8 ;pExeFile与pMuiFile不能相同
jz ss_0
;---装载多资源文件---
invoke LoadLibraryW,pMuiFile
test rax,rax
jz ss_0
mov ss_hInst,rax
;---创建资源更新句柄---
invoke BeginUpdateResourceW,pExeFile,0
test rax,rax
jz ss_out
mov ss_hUpdate,rax
;---更新菜单---
invoke Rsrc_Update,ss_hUpdate,RT_MENU,Id,0,ss_hInst,Id,ulang
;---更新版本资源---
invoke Rsrc_Update,ss_hUpdate,RT_VERSION,VS_VERSION_INFO,\
0,ss_hInst,VS_VERSION_INFO,ulang
;---完成更新---
invoke EndUpdateResource,ss_hUpdate,FALSE
ss_out:
invoke FreeLibrary,ss_hInst ;释放
ss_0:
ret
Localize endp
Rsrc_Update函数是一个通用的资源更新函数,参数说明如下:
(1)hUpdate=目标文件的更新句柄,由BeginUpdateResource函数返回。
(2)rType=要更新的资源类型。涉及到本地化的资源类型主要有以下几类:
RT_MENU: 菜单资源。
RT_DIALOG: 对话框资源。
RT_STRING: 字符串表资源。
RT_MESSAGETABLE: 消息表资源。
RT_VERSION: 版本资源。
(3) wId=要更新的资源ID。这时有2个情况要特别注意,其一是版本资源的ID固定为VS_VERSION_INFO(值为1);其二是字符串表资源是以组为单位的,所以这里的wId是字符串的组号,最小值为1,而不是字符串ID,所以字符串是以组为单位更新的,每组16个字符串(相关内容见《字符串表资源(STRINGTABLE)和程序本地化》一文)。
(4) wlang=要更新的资源语言ID,一般情况下该参数取0。如果被本地化的程序也包含多种语言,则就需要指定要更新的语言ID。例如,如果一个程序包含中文和英文两种语言,在应用时自动切换语言界面,但中文需要进行本地化(简体或繁体中文),这种情况下需要指定要更新的语言ID。
(5) hInst=源资源模块句柄。即包承载有多语言资源的文件模块句柄,由LoadLibrary函数返回。
(6) uId=源模块资源中用于更新的资源ID。一般情况下取与wID相同的值。
(7) ulang=源模块资源中用于更新的资源语言。即指定要使用的语言环境。
3. 通过更新字符串表来实现程序的本地化
如果在编程中凡需要使用字符串的,就一律从资源中获取字符串,这样做的好处是只需对字符串表和版本进行本地化,则可实现对程序的本地化,不需要为每种语言编写资源菜单,也不需要处理消息表资源。
3.1 字符串表本地化
曾经在《字符串表资源(STRINGTABLE)和程序本地化》一文中介绍过字符串表本地化的方法,该方法用于在源资源文件中获取相关的字符串组来对指定的程序进行本地化。以下内容介绍对整个字符串表进行本地化的方法。
版本的本地化非常简单,已在上例中介绍,以下介绍字符串表本化的方法。
将整个字符串资源表进行本地化,需要通过资源枚举的方法。具体函数为Rsrc_UpdataStringTableBkcall和Rsrc_UpdataStringTable,前者为枚举回调函数,后者为字符串表更新主函数,其中所用到的Rsrc_Update函数与上例相同。
;=========================================================
;回调函数---枚举更新
;入: hModule=资源所在文件的实例句柄。
; lpType=资源类型。本例为RT_STRING。
; lpName=资源名。对于字符串表资源为16位一组的组号。
; lParam=Rsrc_UpdataStringTable函数的参数列表数组地址。
;出: RAX=TRUE: 继续
; =0 : 终止
;=========================================================
Rsrc_UpdataStringTableBkcall proc hModule:HANDLE,lpType:QWORD,\
lpName:QWORD,lParam:QWORD
mov rcx,[r9] ;hUpdate ;更新句柄
mov r11,[r9+24] ;uLangId ;语言ID
mov r9,[r9+8] ;wlang
;---更新一组字符串---
invoke Rsrc_Update,rcx,lpType,lpName,r9d,hModule,lpName,r11
mov rax,TRUE
ret
Rsrc_UpdataStringTableBkcall endp
;============================================================
;更新字符串资源表
;入: hUpdate=目标文件的更新句柄(BeginUpdateResource函数返回)
; wlang=要更新的资源语言(一般为0)
; hInst=源资源模块句柄
; ulang=源模块资源中用于更新的资源语言
;出: NO
;============================================================
Rsrc_UpdataStringTable proc hUpdate:QWORD,wlang:QWORD,\
hInst:QWORD,ulang:QWORD
invoke EnumResourceNames,hInst,RT_STRING,\
ADDR Rsrc_UpdataStringTableBkcall,ADDR hUpdate
ret
Rsrc_UpdataStringTable endp
这种情况下要本地化一个程序就更加简单了,可以将上例中的Localize函数改为以下内容:
;=======================================================
;本地化操作主函数
;入: pExeFile=要更新菜单的可执行文件的完整路径文件名
; Unicode串
; pMuiFile=包含多语言资源的DLL完整路径文件名
; Unicode串
; ulang=要使用的资源语言ID。例:
; 404h(1028)--繁体中文。
; 409h(1033)--美国英语。
; 804h(2052)--简体中文。
;出: RAX=1: 成功
; =0: 失败
;=======================================================
Localize2 proc pExeFile:QWORD,pMuiFile:QWORD,ulang:QWORD
LOCAL ss_hInst:QWORD
LOCAL ss_hUpdate:QWORD
cmp rcx,rdx
jz ss_0 ;pExeFile与pMuiFile不能相同
;---装载多资源文件---
invoke LoadLibraryW,pMuiFile
test rax,rax
jz ss_0
mov ss_hInst,rax
;---创建资源更新句柄---
invoke BeginUpdateResourceW,pExeFile,0
test rax,rax
jz ss_out
mov ss_hUpdate,rax
;---更新串表---
invoke Rsrc_UpdataStringTable,ss_hUpdate,0,ss_hInst,ulang
;---更新版本资源---
invoke Rsrc_Update,ss_hUpdate,RT_VERSION,VS_VERSION_INFO,\
0,ss_hInst,VS_VERSION_INFO,ulang
;---完成更新---
invoke EndUpdateResource,ss_hUpdate,FALSE
ss_out:
invoke FreeLibrary,ss_hInst
ss_0:
ret
Localize2 endp
3.2 编写两个必须的API扩展函数
通过更新字符串表来实现程序的本地化,这对编程有些要求。其一是要编写一个字符串资源装载扩展函数;其二是是要编写一个自定义菜单模板装载函数。
3.2.1 编写LoadStringEx函数
Windows有一个LoadString函数,是用于装载资源字符串的,在一般情况下是没有问题的。但软件的本地化有两种方式,方法之一是编写一个专用的程序来对指定的另一个程序进行本地化,而被本地化的软件始终都包含一种语言资源,这种情况下使用LoadString函数就可以满足编程要求;方法之二是不使用专用的程序来对指定的另一个程序进行本地化,而是程序自身通过参数设置来实现界面语言的切换,例程序界面的简体中文与繁体中文的切换,这类程序自身包含多种语言资源,这种情况下使用LoadString函数就不能满足编程要求,需要编写一个LoadStringEx函数。
为了避免与以后API升级可能造成的函数名冲突,使用Rsrc_LoadStringEx函数名。以下为Unicode和Ansi的两个版本的函数源码。
注意:以下所用的StringCbCopyNW、StringCbLengthW函数为字符串安全函数,这是自己写的二个Masm函数,在C/C++环境包含在strsafe.h中。如果没有的话可以使用lstrcpynW和lstrlenW函数。
;=============================================================
;装载一个指定语言的字符串资源---Unicode
;入: hInst=包含字符串资源的实例句柄
; =0: 当前进程
; uId=字符串资源ID
; uLangId=语言ID
; pBuf=字符接收缓冲区地址
; zSize=字符接收缓冲区长度(以WORD为单位)
; 如果缓冲区太小,则返回的串被截断。
; =0: 只返回所需内存数
;出: RAX=字符串的字符个数,不包括结尾符NULL。
; 如果zSize=0,则为所需内存数,包括结尾符NULL,
; (以WORD为单位)。
; =0: 未发现
;=============================================================
Rsrc_LoadStringExW proc hInst:QWORD,uId:QWORD,uLangId:QWORD,\
pBuf:QWORD,zSize:DWORD
test r9,r9 ;pBuf
jnz ss_1
mov zSize,0
jmp ss_2
ss_1:
xor eax,eax
mov [r9],ax ;pBuf初始化
ss_2:
;--------------
; 搜索组
;--------------
;---将字符串ID转换为组序号---
and rdx,0ffffh
shr rdx,4 ;/16
inc rdx
mov r8,rdx
invoke FindResourceExW,hInst,RT_STRING,r8,uLangId
test rax,rax
jz ss_0
invoke LoadResource,hInst,rax ;装入字符串组数组
invoke LockResource,rax ;锁定内存(不需要解锁)
;-----------------
;在组内搜索字符串
;-----------------
mov rdx,uId
and edx,0fh ;该字符串在组内的序号
jz ss_ok ;组首地址
ss_lp1:
xor rcx,rcx
mov cx,[rax] ;Unicide串的字符个数
inc ecx ;长度单元的WORD
shl ecx,1
add rax,rcx ;指向下一个串
dec edx
jnz ss_lp1
ss_ok:
;---复制字符串---
cmp zSize,0
jz ss_retn
xor r9,r9
mov r9w,[rax]
add rax,2
mov edx,zSize
invoke StringCbCopyNW,pBuf,rdx,rax,r9 ;串复制
mov rax,rcx
shr rax,1
ret
ss_retn: ;返回所需内存长度
xor rcx,rcx
mov cx,[rax]
mov rax,rcx
inc rax
ret
ss_no: ;未发现
xor rax,rax
mov rdx,pBuf
test rdx,rdx
jz ss_0
mov [rdx],ax
ss_0:
ret
Rsrc_LoadStringExW endp
;===========================================================
;装载一个指定语言的字符串资源---ANSI
;入: hInst=包含字符串资源的实例句柄
; =0: 当前进程
; uId=字符串资源ID
; uLangId=语言ID
; pBuf=字符接收缓冲区地址
; zSize=字符接收缓冲区字节长度
; =0: 只返回所需内存数
;出: RAX=字符串的字节长度,不包括结尾符NULL。
; 如果 zSize=0,则为所需内存字节长度,包括结尾符NULL。
; =0: 未发现或缓冲区溢出
;===========================================================
Rsrc_LoadStringExA proc hInst:QWORD,uId:QWORD,uLangId:QWORD,\
pBuf:QWORD,zSize:DWORD
LOCAL ss_pMem:QWORD
LOCAL ss_size:DWORD
test r9,r9
jnz ss_1
mov zSize,0
jmp ss_2
ss_1:
xor eax,eax
mov [r9],ax ;pBuf初始化
ss_2:
invoke Rsrc_LoadStringExW,rcx,rdx,r8,0,0 ;计算所需内存
test rax,rax
jz ss_0
inc rax
mov ss_size,eax
shl rax,1
invoke hmAlloc,rax ;分配内存
mov ss_pMem,rax
invoke Rsrc_LoadStringExW,hInst,uId,uLangId,ss_pMem,ss_size
invoke WideCharToMultiByte,CP_ACP,0,ss_pMem,-1,pBuf,zSize,0,0 ;转换为ANSI
mov ss_size,eax
invoke hmFree,ss_pMem ;释放内存
mov eax,ss_size
cmp zSize,0
jz ss_0
test eax,eax
jz ss_ov
dec eax
ss_0:
ret
ss_ov:
mov rdx,pBuf
mov [rdx],ax
ret
Rsrc_LoadStringExA endp
为适应多语言界面的自动切换,需要设置一个全局语言环境参数,并使用对应的函数来读写。一般情况下,我们会将当前语言ID值保存在注册表或初始化文件(.ini)中,程序启动时读取该值,并调用App_SetLocaleId函数设置到程序环境中;程序退出时调用App_GetLocaleId函数将该参数保存到注册表或初始化文件(.ini)中。如果程序中只包含一种语言资源,则该参数为NULL,不需要修改。
源码如下。
.data
App_Locale_Id dq 0 ;程序的当前语言ID(必须初始化为0)
.code
;=================================
;获取程序的语言ID
;返回: RAX=语言ID
;=================================
App_GetLocaleId proc
mov rax,App_Locale_Id
ret
App_GetLocaleId endp
;=================================
;设置程序的语言ID
;参数: uLangId=语言ID
;返回: RAX=uLangId
;=================================
App_SetLocaleId proc uLangId:QWORD
mov App_Locale_Id,rcx
mov rax,rcx
ret
App_SetLocaleId endp
有了语言环境参数,就可以简化字符串资源的装载函数,Rsrc_GetStringW和Rsrc_GetStringA为两个版本的资源字符串装载函数。Rsrc_GetString函数根据用户设置的当前程序语言参数读取对应的资源字符串。
;=====================================================
;获取资源字符串---使用当前程序语言环境
;入: pBuf=字符接收缓冲区地址
; zSize=字符接收缓冲区长度(以WORD为单位)
; 如果缓冲区太小,则返回的串被截断。
; =0: 只返回所需内存数
; uId=字符串资源ID
;出: RAX=字符串的字符个数,不包括结尾符NULL。
; 如果zSize=0,则为所需内存数,包括结尾符NULL,
; (以WORD为单位)。
; =0: 没有对应的字符串
;=====================================================
Rsrc_GetStringW proc pBuf:QWORD,zSize:DWORD,uId:QWORD
call App_GetLocaleId
invoke Rsrc_LoadStringExW,NULL,uId,rax,pBuf,zSize
ret
Rsrc_GetStringW endp
;=====================================================
;获取资源字符串---使用当前程序语言环境
;入: pBuf=字符接收缓冲区地址
; zSize=字符接收缓冲区长度(以BYTE为单位)
; 如果缓冲区太小,则返回的串被截断。
; =0: 只返回所需内存数
; uId=字符串资源ID
;出: RAX=字符串的字符个数,不包括结尾符NULL。
; 如果zSize=0,则为所需内存数,包括结尾符NULL,
; (以BYTE为单位)。
; =0: 没有对应的字符串或缓冲区溢出
;=====================================================
Rsrc_GetStringA proc pBuf:QWORD,zSize:DWORD,uId:QWORD
call App_GetLocaleId
invoke Rsrc_LoadStringExA,NULL,uId,rax,pBuf,zSize
ret
Rsrc_GetStringA endp
应用举例。以下的代码是创建一个按钮控件,如果sId参数为资源字符串"确定"的ID,并且当前程序语言环境参数为804h(简体中文),则界面显示时该按钮的标题为"确定",当前程序语言环境参数为404h(繁体中文),则界面显示时该按钮的标题为"確定"。
;==================================================
;创建一个按钮控件
;入: hWin=主窗口句柄
; sId=按钮标题字符串的资源ID
; x,y,wd,ht=控件起始坐标,宽和高
; cId=控件ID
;出: RAX=控件句柄
;==================================================
new_btn proc hWin:HWND,sId:QWORD,x:DWORD,y:DWORD,\
wd:DWORD,ht:DWORD,cId:DWORD
LOCAL ss_str[MAX_PATH]:BYTE
;---从资源中取字符串---
invoke Rsrc_GetStringA,ADDR ss_str,MAX_PATH,sId
;---获取默认实例句柄---
rcall GetModuleHandle,NULL
mov r10,rax
;---创建按钮控件---
invoke CreateWindowExA,0,"BUTTON",ADDR ss_str,WS_CHILD or WS_VISIBLE,\
x,y,wd,ht,hWin,cId,r10,NULL
ret
new_btn endp
为了动态设置窗口或控件的标题,需要编写一个SetWindowTextEx函数来替换SetWindowText。例它的Unicode版本源码如下:
;====================================================
;设置窗口(或控件)的文本---SetWindowText函数扩展
;入: hWin=窗口或控件句柄
; sId=窗口或控件标题字符串的资源ID或字符串地址。
; 如果该值>0ffffh,则为字符串地址。
;出: RAX=SetWindowText函数的返回码
;====================================================
SetWindowTextExW proc hWin:QWORD,sId:QWORD
LOCAL ss_str[MAX_PATH]:WORD
cmp rdx,0ffffh ;sId
jnbe ss_1
invoke Rsrc_GetStringW,ADDR ss_str,MAX_PATH,sId
lea rdx,ss_str
ss_1:
invoke SetWindowText,hWin,rdx
ret
SetWindowTextExW endp
3.2.2 编写LoadMenuIndirectEx函数
在《增加LoadMenuIndirectEx函数》一文中介绍过自定义扩展菜单模板的装载函数LoadMenuIndirectEx,这里直接给出改进后的LoadMenuIndirectEx函数。
下面的函数中用到一个MENUEXEX_TEMPLATE_HEADER结构,该结构是对Windows的MENUEX_TEMPLATE_HEADER结构的扩展,即增加首个成员iCodePage作为模板中字符串常量的代码页,如果菜单模板中全部使用字符串资源ID,则该成员可以为NULL。有关MENUEX_TEMPLATE_HEADER和MENUEX_TEMPLATE_ITEM结构可以查阅Windows文档。
iCodePage成员可以为以下值之一:
CP_ACP或CP_WINANSI: 系统默认的Windows ANSI代码页
CP_UTF8 : UTF-8
CP_WINUNICODE : Unicode
MENUEXEX_TEMPLATE_HEADER结构定义如下:
MENUEXEX_TEMPLATE_HEADER STRUCT
iCodePage QWORD ? ;代码页
MENUEX_TEMPLATE_HEADER <>
MENUEXEX_TEMPLATE_HEADER ENDS
以下是自定义扩展菜单模板的装载函数,共有4个函数。
Menu_GetTempItemInfoEx :用于解读模板数据的子函数。
Menu_LoadTemplateItemEx :用递归方法创建各菜单项的子函数。
Menu_LoadTemplateEx :菜单模板装载主函数。
LoadTemplateEx :根据当前语言环境参数设置,从当前模块中装载菜单模板函数。
;====================================================
;获取一个菜单项模板数据---自定义扩展模板
;入: pTemp=当前菜单项模板MENUEX_TEMPLATE_ITEM结构地址
; cPage=字符串代码页
; pInfo=MENUITEMINFO结构地址。用于接收信息.
; .dwTypeData=文本接收缓冲区地址。
; 字节长度>=MAX_PATH*2
; pFlag=DWORD变量地址。用于接收wFlags成员
; pHelpId=DWORD变量地址。用于接收dwHelpId成员
; =0: 不用
; hInst=字符串资源所在的模块实例句柄
; =0: 为本进程模块
; uLangId=字符串资源语言ID。例:
; 409h--美国英语
; 804h--简体中文
; 404h--繁体中文
; =0: 默认为本地语言
;出: RAX=被处理的结构的字节长度
; .hSubMenu=子菜单句柄(如果为MFR_POPUP类型)
; EDX=dwHelpId成员值(0=无)
;====================================================
Menu_GetTempItemInfoEx proc pTemp:QWORD,cPage:QWORD,\
pInfo:QWORD,pFlag:QWORD,pHelpId:QWORD,\
hInst:QWORD,uLangId:QWORD
LOCAL ss_rbx:QWORD
LOCAL ss_rsi:QWORD
LOCAL ss_flag:QWORD
LOCAL ss_slen:QWORD
mov ss_rbx,rbx
mov ss_rsi,rsi
mov rsi,rcx ;pTemp
mov rbx,r8 ;pInfo=MENUITEMINFO结构地址
mov [rbx.MENUITEMINFO].cbSize,SIZEOF MENUITEMINFO
mov [rbx.MENUITEMINFO].hbmpChecked,0
mov [rbx.MENUITEMINFO].hbmpUnchecked,0
mov [rbx.MENUITEMINFO].hbmpItem,0
mov [rbx.MENUITEMINFO].fMask,MIIM_STATE or MIIM_FTYPE
;---取基本参数---
mov eax,[rsi.MENUEX_TEMPLATE_ITEM].dwType
mov [rbx.MENUITEMINFO].fType,eax
mov eax,[rsi.MENUEX_TEMPLATE_ITEM].dwState
mov [rbx.MENUITEMINFO].fState,eax
mov eax,[rsi.MENUEX_TEMPLATE_ITEM].uId
mov [rbx.MENUITEMINFO].wID,eax
xor rax,rax
mov ax,[rsi.MENUEX_TEMPLATE_ITEM].wFlags
mov ss_flag,rax
mov [r9],eax
add rsi,SIZEOF MENUEX_TEMPLATE_ITEM ;结构的基本字节长度
;---取其他项目---
test ax,MFR_POPUP
jnz ss_sub
test [rbx.MENUITEMINFO].fType,MFT_SEPARATOR
jnz ss_sp
or [rbx.MENUITEMINFO].fMask,MIIM_ID
jmp ss_str
;---分隔条---
ss_sp:
add rsi,2 ;移过空串
mov [rbx.MENUITEMINFO].fMask,MIIM_TYPE
mov [rbx.MENUITEMINFO].fType,MFT_SEPARATOR
jmp ss_ok
;---子菜单---
ss_sub:
or [rbx.MENUITEMINFO].fMask,MIIM_SUBMENU
call CreatePopupMenu ;创建子菜单
mov [rbx.MENUITEMINFO].hSubMenu,rax
;---菜单项目串---
ss_str:
or [rbx.MENUITEMINFO].fMask,MIIM_STRING
test ss_flag,MFR_TXTID
jnz ss_strid
;---为字符串值---
cmp cPage,CP_WINANSI
jz ss_ansi
cmp cPage,CP_WINUNICODE
jnz ss_tow
;---Unicode---
invoke StringCbCopyW,QWORD PTR [rbx.MENUITEMINFO].dwTypeData,MAX_PATH,rsi
mov ss_slen,0
invoke StringCbLengthW,rsi,MAX_PATH,ADDR ss_slen ;返回字符串长度
mov rax,ss_slen
inc rax ;串结尾符个数
shl rax,1 ;转为字节长度
add rsi,rax
jmp ss_3
ss_ansi:
mov cPage,CP_ACP
ss_tow: ;将字符串转换为Unicode
mov r10,[rbx.MENUITEMINFO].dwTypeData
invoke MultiByteToWideChar,cPage,0,rsi,-1,r10,MAX_PATH
mov ss_slen,0
invoke StringCbLengthA,rsi,MAX_PATH,ADDR ss_slen
mov rax,ss_slen
inc rax ;串结尾符个数
add rsi,rax
ss_3:
;---指针移到字符后须经4字节对齐---
add rsi,3
and rsi,NOT 3
jmp ss_help
;---为字符串资源ID---
ss_strid:
xor rax,rax
lodsw ;字符串ID
test ax,ax
jnz ss_6
mov eax,[rbx.MENUITEMINFO].wID ;使用菜单ID作为字符串资源ID
ss_6:
mov rdx,rax
mov r8,uLangId
mov r9,[rbx.MENUITEMINFO].dwTypeData
test r8,r8
jz ss_7
invoke Rsrc_LoadStringExW,hInst,rdx,r8,r9,MAX_PATH*2
jmp ss_help
ss_7: ;装入与语言无关的字符串资源
mov r8,r9
invoke LoadStringW,hInst,edx,r8,MAX_PATH
;---子菜单帮助标识(HelpID)---
ss_help:
xor rdx,rdx
test ss_flag,MFR_POPUP
jz ss_ok ;不是子菜单时无该项目
lodsd
mov edx,eax
mov rcx,pHelpId
test rcx,rcx
jz ss_ok
mov [rcx],edx
ss_ok:
mov rax,rsi
sub rax,pTemp
mov rbx,ss_rbx
mov rsi,ss_rsi
ret
Menu_GetTempItemInfoEx endp
;============================================================
;菜单模板项目装载---自定义扩展模板
;入: uhMenu=菜单句柄
; item=新菜单项的插入位置,其意义由zFlag指定。
; zFlag=FALSE: item为插入处的菜单项目ID
; TURE: item为插入的菜单项位置(0...)
; pTemp=首个MENUEX_TEMPLATE_ITEM结构
; cPage=字符串代码页
; hInst=字符串资源所在的模块实例句柄
; =0: 为本进程模块
; uLangId=字符串资源语言ID。例:
; 409h--美国英语
; 804h--简体中文
; 404h--繁体中文
; =0: 与语言无关(即程序资源只包含一种语言的串表)
;出: RAX=指向下一个MENUEX_TEMPLATE_ITEM结构(递归使用)
;============================================================
Menu_LoadTemplateItemEx proc uhMenu:QWORD,item:DWORD,\
zFlag:DWORD,pTemp:QWORD,cPage:DWORD,\
hInst:QWORD,uLangId:QWORD
LOCAL ss_mi:MENUITEMINFO
LOCAL ss_buf[MAX_PATH]:WORD
LOCAL ss_rsi:QWORD
LOCAL ss_Flag:DWORD
mov ss_rsi,rsi
mov rsi,r9
cmp hInst,0
jnz ss_lp1
invoke GetModuleHandle,NULL
mov hInst,rax
ss_lp1:
;---4字节对齐---
add rsi,3
and rsi,NOT 3
;---取MENUEX_TEMPLATE_ITEM结构数据---
lea rax,ss_buf
mov ss_mi.dwTypeData,rax
invoke Menu_GetTempItemInfoEx,rsi,cPage,ADDR ss_mi,ADDR ss_Flag,0,hInst,uLangId
add rsi,rax
;---添加项目---
invoke InsertMenuItemW,uhMenu,item,zFlag,ADDR ss_mi
test eax,eax
jz ss_out
cmp zFlag,0
jz ss_5
inc item
ss_5:
test ss_mi.fMask,MIIM_SUBMENU
jz ss_nt
invoke Menu_LoadTemplateItemEx,ss_mi.hSubMenu,0,TRUE,rsi,cPage,hInst,uLangId ;递归
mov rsi,rax
ss_nt:
test ss_Flag,MFR_END
jz ss_lp1
ss_out:
mov rax,rsi ;递归使用
mov rsi,ss_rsi
ret
Menu_LoadTemplateItemEx endp
;================================================
;菜单模板装载---自定义扩展模板
;入: pTemp=自定义扩展模板地址。结构如下:
; MENUEXEX_TEMPLATE_HEADER <>
; MENUEX_TEMPLATE_ITEM <>
; ...
; zFlag=标志:
; MF_POPUP: 装入为快捷菜单
; hInst=字符串资源所在的模块实例句柄
; =0: 为本进程模块
; uLangId=字符串资源语言ID。例:
; 409h--美国英语
; 804h--简体中文
; 404h--繁体中文
; =0: 默认为本地语言
;出: RAX=菜单句柄
;----------------------------------------------
;注:MENUEXEX_TEMPLATE_HEADER结构的iCodePage成员
; 为字符串代码页,即模板中字符串常量的编码:
; CP_ACP或CP_WINANSI:系统默认的Windows ANSI代码页
; CP_UTF8 :UTF-8
; CP_WINUNICODE :Unicode
;================================================
Menu_LoadTemplateEx proc pTemp:QWORD,zFlag:DWORD,\
hInst:QWORD,uLangId:QWORD
LOCAL ss_hMenu:QWORD
LOCAL ss_cPage:QWORD
cmp hInst,0
jnz ss_be
invoke GetModuleHandle,NULL
mov hInst,rax
ss_be:
test zFlag,MF_POPUP
jnz ss_popup
invoke CreateMenu
jmp ss_2
ss_popup:
invoke CreatePopupMenu
ss_2:
mov ss_hMenu,rax
mov r9,pTemp
mov rax,[r9.MENUEXEX_TEMPLATE_HEADER].iCodePage ;代码页
mov ss_cPage,rax ;串代码页
xor rax,rax
mov ax,[r9.MENUEXEX_TEMPLATE_HEADER].wOffset
lea r9,[r9.MENUEXEX_TEMPLATE_HEADER].wOffset
add r9,rax ;指向首个MENUEX_TEMPLATE_ITEM结构
invoke Menu_LoadTemplateItemEx,ss_hMenu,0,TRUE,r9,ss_cPage,hInst,uLangId
mov rax,ss_hMenu
ret
Menu_LoadTemplateEx endp
;================================================
;根据当前语言环境参数设置,装载菜单模板函数。
;入: pTemp=自定义扩展模板地址。结构如下:
; MENUEXEX_TEMPLATE_HEADER <>
; MENUEX_TEMPLATE_ITEM <>
; ...
; zFlag=标志:
; MF_POPUP: 装入为快捷菜单
;出: RAX=菜单句柄
;================================================
LoadTemplateEx proc pTemp:QWORD,zFlag:DWORD
call App_GetLocaleId
invoke Menu_LoadTemplateEx,pTemp,zFlag,NULL,rax
ret
LoadTemplateEx endp
3.3 更新字符串表实现程序本地化的实例
3.3.1 编写资源文件
先写一个资源头文件,文件名指定为"mui.h",与上例不同,这个头文件只定义字符串ID,不需要定义菜单ID。内容如下:
// 字符串ID
#define IDRS_FILE 1
#define IDRS_NEW 2
#define IDRS_OPEN 3
#define IDRS_SAVE 4
#define IDRS_SAVEAS 5
#define IDRS_CLOSE 6
#define IDRS_EDIT 10
#define IDRS_UNDO 11
#define IDRS_CUT 12
#define IDRS_COPY 13
#define IDRS_PASTE 14
#define IDRS_DEL 15
#define IDRS_HELP 20
#define IDRS_ABOUT 21
然后写各语言的版本信息和资源字符串表。如果程序需要在繁体中文,英语和简体中文环境中使用,则需写3个资源文件,文件名分别为"rsrc_0404.rc"、“rsrc_0409.rc”、“rsrc_0804.rc”。
rsrc_0404.rc文件内容如下:
//==================================
// 繁体中文
//==================================
LANGUAGE 4,1 //0x404
//=================================
// 版本信息
//=================================
VS_VERSION_INFO VERSIONINFO
FILEVERSION 1, 0, 0, 0
PRODUCTVERSION 1, 0, 0, 0
FILEOS VOS__WINDOWS32
FILETYPE VFT_APP
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040404B0"
BEGIN
VALUE "CompanyName", "The MASM32 SDK\000"
VALUE "FileDescription", "Default Template\000"
VALUE "FileVersion", "1.0\000"
VALUE "InternalName", "template\000"
VALUE "OriginalFilename", "template.exe\000"
VALUE "LegalCopyright", "\251 2016 The MASM32 SDK\000"
VALUE "ProductName", "template\000"
VALUE "ProductVersion", "1.0\000"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x404, 0x4B0
END
END
//=========================
// 字符串表定义---繁体中文
//=========================
STRINGTABLE
BEGIN
IDRS_FILE "文件"
IDRS_NEW "新建"
IDRS_OPEN "打開"
IDRS_SAVE "保存"
IDRS_SAVEAS "另存為"
IDRS_CLOSE "關閉"
IDRS_HELP "幫助"
IDRS_ABOUT "關於"
IDRS_EDIT "編輯"
IDRS_UNDO "取消"
IDRS_CUT "剪切"
IDRS_COPY "復製"
IDRS_PASTE "粘貼"
IDRS_DEL "删除"
END
rsrc_0409.rc文件内容如下:
//=================================
// 美国英语
//=================================
LANGUAGE 9,1 //0x409
//=================================
// 版本信息
//=================================
VS_VERSION_INFO VERSIONINFO
FILEVERSION 1, 0, 0, 0
PRODUCTVERSION 1, 0, 0, 0
FILEOS VOS__WINDOWS32
FILETYPE VFT_APP
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904B0"
BEGIN
VALUE "CompanyName", "The MASM32 SDK\000"
VALUE "FileDescription", "Default Template\000"
VALUE "FileVersion", "1.0\000"
VALUE "InternalName", "template\000"
VALUE "OriginalFilename", "template.exe\000"
VALUE "LegalCopyright", "\251 2016 The MASM32 SDK\000"
VALUE "ProductName", "template\000"
VALUE "ProductVersion", "1.0\000"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 0x4B0
END
END
//=========================
// 字符串表定义---美国英语
//=========================
STRINGTABLE
BEGIN
IDRS_FILE "File"
IDRS_NEW "New"
IDRS_OPEN "Open"
IDRS_SAVE "Save"
IDRS_SAVEAS "SaveAs"
IDRS_CLOSE "Close"
IDRS_HELP "Help"
IDRS_ABOUT "About"
IDRS_EDIT "Edit"
IDRS_UNDO "Undo"
IDRS_CUT "Cut"
IDRS_COPY "Copy"
IDRS_PASTE "Paste"
IDRS_DEL "Delete"
END
rsrc_0804.rc文件内容如下:
//=================================
// 简体中文
//=================================
LANGUAGE 4,2 //0x804
//=================================
// 版本信息
//=================================
VS_VERSION_INFO VERSIONINFO
FILEVERSION 1, 0, 0, 0
PRODUCTVERSION 1, 0, 0, 0
FILEOS VOS__WINDOWS32
FILETYPE VFT_APP
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "080404B0"
BEGIN
VALUE "CompanyName", "The MASM32 SDK\000"
VALUE "FileDescription", "Default Template\000"
VALUE "FileVersion", "1.0\000"
VALUE "InternalName", "template\000"
VALUE "OriginalFilename", "template.exe\000"
VALUE "LegalCopyright", "\251 2016 The MASM32 SDK\000"
VALUE "ProductName", "template\000"
VALUE "ProductVersion", "1.0\000"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x804, 0x4B0
END
END
//=========================
// 字符串表定义---简体中文
//=========================
STRINGTABLE
BEGIN
IDRS_FILE "文件"
IDRS_NEW "新建"
IDRS_OPEN "打开"
IDRS_SAVE "保存"
IDRS_SAVEAS "另存为"
IDRS_CLOSE "关闭"
IDRS_HELP "帮助"
IDRS_ABOUT "关于"
IDRS_EDIT "编辑"
IDRS_UNDO "取消"
IDRS_CUT "剪切"
IDRS_COPY "复制"
IDRS_PASTE "粘贴"
IDRS_DEL "删除"
END
然后写一个包含全部语言的资源文件,文件名指定为"mui.rc",内容如下:
#include "\masm32\include64\resource.h"
#include <mui.h>
rcinclude Rsrc_0404.rc
rcinclude Rsrc_0804.rc
rcinclude Rsrc_0409.rc
创建一个承载多语言资源的EXE文件与上例方法相同,这里不再重述。
3.3.2 编写菜单模板
菜单模板不是定义在资源文件中,而是在源码文中定义菜单模板。不管程序可本地化几种语言,菜单模板只需要一个,不必为每种语言写一个模板。
以下为自定义扩展菜单模板。自定义扩展菜单模板结构与Windows扩展菜单模板是相同的,唯一的区别是MENUEX_TEMPLATE_HEADER结构扩展为MENUEXEX_TEMPLATE_HEADER,增加一个代码页成员。同时MENUEX_TEMPLATE_ITEM结构的wFlags成员增加了一个MFR_TXTID位值。
;---字符串ID---
IDRS_FILE EQU 1
IDRS_NEW EQU 2
IDRS_OPEN EQU 3
IDRS_SAVE EQU 4
IDRS_SAVEAS EQU 5
IDRS_CLOSE EQU 6
IDRS_EDIT EQU 10
IDRS_UNDO EQU 11
IDRS_CUT EQU 12
IDRS_COPY EQU 13
IDRS_PASTE EQU 14
IDRS_DEL EQU 15
IDRS_HELP EQU 20
IDRS_ABOUT EQU 21
;---菜单ID---
IDM_FILE EQU 100
IDM_NEW EQU 101
IDM_OPEN EQU 102
IDM_SAVE EQU 103
IDM_SAVEAS EQU 104
IDM_CLOSE EQU 105
IDM_EDIT EQU 200
IDM_UNDO EQU 201
IDM_CUT EQU 202
IDM_COPY EQU 203
IDM_PASTE EQU 204
IDM_DEL EQU 205
IDM_HELP EQU 300
IDM_ABOUT EQU 301
;========================================================
;MENUEX_TEMPLATE_ITEM中的wFlags标志
;增加一个MFR_TXTID,作用如下:
;(1)如果wFlags标志无MFR_TXTID,则szText成员为字符串。
;(2)如果wFlags标志有MFR_TXTID,则szText成员为字符串资源ID。
;========================================================
MFR_END EQU 80h ;一个菜单条中的最后一个菜单项。
MFR_POPUP EQU 01h ;子菜单标题项。
MFR_TXTID EQU 100h ;szText成员指示标志
;-----------------------------------------
;菜单模板结构
;-----------------------------------------
;---自定义的模板头结构---
MENUEXEX_TEMPLATE_HEADER STRUCT
iCodePage QWORD ? ;代码页
MENUEX_TEMPLATE_HEADER <>
MENUEXEX_TEMPLATE_HEADER ENDS
;---Windows定义的描述性菜单模板结构---
MENUEX_TEMPLATE_HEADER STRUCT
wVersion WORD ?
wOffset WORD ?
dwHelpId DWORD ?
MENUEX_TEMPLATE_HEADER ENDS
MENUEX_TEMPLATE_ITEM STRUCT
dwType DWORD ?
dwState DWORD ?
uId DWORD ?
wFlags WORD ?
;MENUEX_TEMPLATE_ITEM ENDS
; szText 菜单字符串
; dwHelpId DWORD ? ;只有当wFlags有MFR_POPUP时,才有该成员,否则无该成员。
; 4字节对齐
;--------------------------------------
;扩展菜单模板中的字符串常量代码页。
;这些常量定义在编程系统头文件中。
;--------------------------------------
; CP_ACP或CP_WINANSI:系统默认的Windows ANSI代码页
; CP_UTF8 :UTF-8
; CP_WINUNICODE :Unicode
;--------------------------------------
.data
;-----------------
; 扩展菜单模板
;-----------------
align 4
Menu_Temp MENUEXEX_TEMPLATE_HEADER <CP_WINUNICODE,1,4,0>
;---以下紧跟菜单项(每项一个MENUEX_TEMPLATE_ITEM结构)---
;---File子菜单---
dd MFT_STRING ;菜单项类型
dd MFS_ENABLED ;菜单项状态
dd IDM_FILE ;菜单项ID
dw MFR_POPUP or MFR_TXTID
dw IDRS_FILE ;菜单标题字符串资源ID
align 4 ;字符串后面须为4字节对齐
dd 0 ;MFR_POPUP类子菜单帮助标识(保留)。
;--------------
align 4 ;每项须4字节对齐(因为使用字符串资源ID,为自然对齐,所以下面省略)
dd MFT_STRING ;菜单项类型
dd MFS_ENABLED ;菜单项状态
dd IDM_NEW ;菜单项ID
dw MFR_TXTID
dw IDRS_NEW
;--------------
dd 0,0,IDM_OPEN ;MFT_STRING=0,MFS_ENABLED=0
dw MFR_TXTID
dw IDRS_OPEN
;--------------
dd 0,0,IDM_SAVE
dw MFR_TXTID
dw IDRS_SAVE
;--------------
dd 0,0,IDM_SAVEAS
dw MFR_TXTID
dw IDRS_SAVEAS
;--------------
dd 0,0,IDM_CLOSE
dw MFR_END or MFR_TXTID ;注意: 这是该子菜单的最后一项
dw IDRS_CLOSE
;---Edit子菜单---
dd 0,0,IDM_EDIT
dw MFR_POPUP or MFR_TXTID
dw IDRS_EDIT
dd 0 ;(保留)
;-----------------
dd 0,0,IDM_UNDO
dw MFR_TXTID
dw IDRS_UNDO
;-----------------
dd 0,0,IDM_CUT
dw MFR_TXTID
dw IDRS_CUT
;-----------------
dd 0,0,IDM_COPY
dw MFR_TXTID
dw IDRS_COPY
;-----------------
dd 0,0,IDM_PASTE
dw MFR_TXTID
dw IDRS_PASTE
;-----------------
dd MFT_SEPARATOR ;分隔线
dd 0,0
dw 0,0
;-----------------
dd 0,0,IDM_DEL
dw MFR_END or MFR_TXTID ;注意: 这是该子菜单的最后一项
dw IDRS_DEL
;---Help子菜单---
dd 0,0,IDM_HELP
dw MF_END or MFR_POPUP or MFR_TXTID ;注意: 这是主菜单的最后一项
dw IDRS_HELP
dd 0 ;(保留)
;-----------------
dd 0,0,IDM_ABOUT
dw MFR_END or MFR_TXTID ;注意: 这是该子菜单的最后一项
dw IDRS_ABOUT
创建菜单就直接调用LoadTemplateEx函数,例:
invoke LoadTemplateEx,ADDR Menu_Temp,0
4. 项目组织
在编写可本地化程序时,因为资源文件很多,所以可以将多资源文件单独创建一个子目录,以下根据上面的例子进行说明。
如果"Test"目录下编写一个Test.exe程序,则在"Test"目录下创建一个子目录"MuiRsrc",然后将上例的以下文件存放在"Test\MuiRsrc"子目录下:
Rsrc_0404.rc
Rsrc_0409.rc
Rsrc_0804.rc
mui.h
再在"Test"目录中编写一个当前默认资源文件,文件名指定为"rsrc.rc",在该资源文件中包含一个当前默认资源,如简体中文(Rsrc_0804.rc)。内容如下:
#include "\masm32\include64\resource.h"
#include <MuiRsrc\mui.h>
#include <MuiRsrc\Rsrc_0804.rc>
这样组织项目,可以共用资源文件,且结构清楚。