Windows菜单本地化方法

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>

这样组织项目,可以共用资源文件,且结构清楚。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值