利用钩子函数动态汉化外文程序菜单
杨山河
引言
当前,英文版的应用程序,一般在中文平台上就相应有中文版。这其中有软件商自己推出的中文语言本地化版本,也有国内或国外的第三方供应商为了市场需要而推出中文版本。后者汉化的方式又分内核汉化和外挂汉化。由于内核汉化涉及到版权等一系列问题,加之源代码设计不一定与中文兼容,因而很少有人采用。因此,在今天的中国,从中文之星到四通立方均采用外挂汉化的方式。而外挂汉化又可以采用多种途径,并且根据所处的操作系统平台的语种不同(简体中文、繁体中文、英文等),汉化的程度也不同。笔者对其中的技术略有了解,通过研究,在此提出一种在中文操作系统平台上外挂汉化的方法,供感兴趣的同仁讨论。
在此之前,我们必须明确几个相关概念。
二.Windows的“钩子”
同PC上的老资格操作系统DOS编程不同,Windows下编程必须得遵守微软给大家规定的“游戏规则” :建立一个窗口,为该窗口建立一个消息循环,在窗口函数中分段完成各种工 作,总之:必须听从微软的安排,否则,你的应用程序就是不符合规范的 ,难以保证兼容性。其实,微软留下很多“后门”,“钩子函数”就是其中手段之一。
所谓“钩子函数”,即是的消息处理机制中留给应用程序的一个手段,应用程序可以利用它安装一个针对特定的消息的子例程,使得我们可以在某些消息在到达目的地之前监视或修改它们。Windows提供了7种钩子函数, Windows 95有13种。鉴于操作系统的发展趋势,不作Windows3.x的讨论,以下提到Windows时指Windows 95(简称Win95)。安装和卸下这些钩子函数的方法是相同的,Win95使用以下API:
HHOOK SetWindowsHookEx(//安装
int idHook, //要安装的钩子的类型
HOOKPROC lpfn,// 钩子函数地址(回调函数)
HINSTANCE hMod,// 钩子函数所在程序的实例句柄
DWORD dwThreadId//要为之安装钩子的线程的ID
);
当一些应用程序安装同一类的钩子时,就会形成一个钩子函数链。安装函数返回指定钩子类型所安装的过滤函数实例句柄。。
BOOL UnhookWindowsHookEx(//卸下
HHOOK hhk //将要被卸下的钩子函数的句柄
);
Win95下的钩子类型有:
WH_CALLWNDPROC //窗口过程钩子
WH_CALLWNDPROCRET //消息已在窗口过程处理
//后,接收这些消息的钩子函数
WH_CBT //基于计算机的训练钩子
WH_DEBUG //查错钩子
WH_FOREGROUNDIDLE //前台空闲窗口钩子
WH_GETMESSAGE //接收投递消息的钩子
WH_JOURNALPLAYBACK //回放以前输入消息钩子
WH_JOURNALRECORD //记录输入消息钩子
WH_KEYBOARD //键盘钩子
WH_MOUSE //鼠标器钩子
WH_MSGFILTER //对话框等的消息钩子
WH_SYSMSGFILTER //系统消息钩子
WH_SHELL //外壳钩子
通常将钩子函数放在DLL中,使得它可供系统内的每一个进程访问,因此安装钩子API中的第三个参数应该是DLL模块的句柄。
在这些类型的钩子中,WH_GETMESSAGE类型的钩子可以为我们存取窗口的信息服务。该钩子截获所有通过标准的消息循环获取函数GetMessage从应用队列获取的消息。这些消息当中包含着很多有价值的数据,譬如:窗口句柄等。
一般情况下,钩子函数无论所关联的进程是否活动,均会被调用,会降低系统的性能,甚至破坏系统稳定。因此,钩子函数的使用应该慎重,并且使用完毕,应立即卸下。Win95是一个多线程的系统,需要使用者注意所用的钩子函数的作用范围。以下是作用范围的说明:
钩子 作用范围
WH_CALLWNDPROC 线程或系统
WH_CBT 线程或系统
WH_DEBUG 线程或系统
WH_GETMESSAGE 线程或系统
WH_JOURNALPLAYBACK 仅系统
WH_JOURNALRECORD 仅系统
WH_KEYBOARD 线程或系统
WH_MOUSE 线程或系统
WH_MSGFILTER 线程或系统
WH_SHELL 线程或系统
WH_SYSMSGFILTER 仅系统
利用钩子函数实现汉化
如果我们知道一个窗口的句柄,我们就可以GetMenu函数获得
该窗口的菜单句柄。之后,我们可以通过以下菜单函数进行每一菜单项的外文字符串获取,通过查字典可以获取相应的中文字符串,最后将中文字符串对外文字符串进行替换。注意,中文的显示本程序并不能解决,所以需要在中文操作系统上执行。这里的以上过程可以表示为:
hWnd(窗口句柄)
GetMenu( hWnd)
hMenu(菜单句柄)
GetSubMenu(hMenu,flag,position)
hSubMenu(子菜单句柄)
GetMenuItemString(hSubMenu,position,lpString,len,flag)
lpszText(菜单项外文字符串)
FindChinese(lpString,lpChinese,len)
lpszChinese(中文字符串)
ModifyMenu(hSubMenu,position,flag,id,lpChinese)
菜单项汉化
那么,如何从一个程序获取另外一个程序的窗口句柄呢?这必须知道窗口的窗口类。因为窗口类名是一个全局原子,不可能和另外的类名相同,可以作为唯一标识 。通过类名,FindWindowEx可以获取它的当前正在运行的应用程序实例的窗口句柄。
通过以上分析,我们可以提出这样一个方案:
运行一个特定的应用程序,该程序完成两项任务:
搜索我们的外文版程序的类名,获取该程序当前实例的窗口句柄、主线程的ID,为其安装一个WH_GETMESSAGE类型的钩子函数。利用窗口句柄获取主菜单句柄,在菜单后面加上一个自定义的菜单项。该菜单项主要用来向窗口过程发送消息,以便WH_GETMESSAGE类型的钩子函数能够激活,并且获得外文
程序的主菜单的句柄,进行上述分析的汉化过程。
2.
钩子函数主要进行汉化工作。钩子函数应该存在与一个DLL中。主要用来截获自定义的菜单消息,在此菜单消息处理中,获取主菜单句柄,查找DLL中的字典,替换外文菜单项。
程序说明
由于Win95是多线程环境,因而给程序挂接钩子函数时必须指定线程ID。在Windows 3.x环境下,该参数通常置为 NULL。
钩子安装程序在寻找窗口类时,若找不到,应该根据设定的有关外文程序的路径,利用Winexec函数启动之,再来寻找,以便可以顺利安装钩子函数。
钩子安装程序通过获得窗口句柄,此时也可以进行菜单汉化。但考虑到许多应用程序在运行当中往往动态修改菜单,因而将汉化的主要过程放在钩子函数中。因为在卸下前钩子总有效,所以通过自定义菜单项的选取,就可以方便的进行动态汉化。
钩子安装程序和我们要汉化的外文程序是两个不同的进程,在Win95下分别享有两个不同的应用程序空间。要实现动态连接库的字典为外文程序所用,因为钩子函数挂接后,同所关联的线程属同一进程空间,而DLL中的字典数据属于钩子安装程序,所以必须将字典和一些钩子函数用到的变量设置成存储器共享,需用到#pragma date_seg指令。具体见程序代码。这一点很重要,否则会出现令人讨厌的保护错。
为简单起见,动态连接库中的字典词条数量有限。实际上,Win95下,可以大于64K。另,示例中采用的模式匹配极为简单,大家可以在次基础上进行改进。
菜单中往往包含有许多的键盘操作简捷键,所以中文字符串必须要继承外文字符串中的相关内容,原程序的所有操作必须不能被改变。。
替换菜单项后,必须马上进行菜单条的刷新,否则会使菜单的显示极为混乱。具体应在替换后立即调用DrawMenuBar函数。
要定义好.def文件,对函数的导出、引入必须准确,否则会导致编译失败。
程序在Pwin95、BC5.0,PMMX166环境下通过。可能需要BC的一些动态连接公用库,调试时注意。
10为起到演示目的、缩减篇幅,在此将程序的部分代码进行注释。
钩子函数安装程序:
#define STRICT
#include <windows.h>
#include <windowsx.h>
#include "mate.h"
BOOL WINAPI _import InstallMsgFilterHook(HWND hWnd);
int PASCAL WinMain(HINSTANCE hInstance,HINSTANCE hPre, LPSTR lpCmdLine, int nCmdShow)
{
char szProgramClass[30]="shi";//外文程序的类名,此处为本演示//程序的类名
char szProgramPath[30]="e://ysh//shi//shi.exe";//外文程序路径,此
//处可以设置成读者自定义的要被汉化的程序路径
MSG msg;
HWND _hWnd; //要被挂接钩子的外文程序句柄
HMENU hMenu; //主菜单句柄
int nTopLevel; //顶级弹出菜单的项数
_hWnd=FindWindow( szProgramClass,NULL);
if(_hWnd= =NULL) //未找到
{
if(WinExec(szProgramPath,SW_SHOW)==ERROR_FILE_NOT_FOUND) //未找到指定的程序
{
MessageBox(NULL,"File not found!","中文伴侣",MB_OK);
return(FALSE);
}
_hWnd=FindWindow(szProgramClass,NULL);//启动之后再次寻找
if(_hWnd==NULL)
{
MessageBox(NULL,"Find Window Faild!","中文伴侣",MB_OK);
return(FALSE);
}
}
LoadLibrary("mate.dll"); //装入库
hMenu=GetMenu(_hWnd); //获取菜单
AppendMenu(hMenu,MF_BITMAP|MF_ENABLED,CM_MYMENUITEM,"中文&&英文"); //增加自己的菜单项
DrawMenuBar(_hWnd); //重画菜单条,否则菜单显示混乱
if(!InstallMsgFilterHook(_hWnd))//安装钩子
{
MessageBox(NULL,"钩子函数安装失败!","中文伴侣",MB_OK);
return(FALSE);
}
}
return(TRUE);
}
动态连接库的主要代码:
#define COMMONNUM 100
#include <windows.h>
#include <windowsx.h>
#include <string.h>
#include "mate.h"
#pragma data_seg("Tweny") / /须在.def中说明成共享段
static HHOOK MsgFilterHook=NULL;//接收投递消息钩子句柄
static HINSTANCE hInstance=NULL;
static HWND shihWnd=NULL; //外文程序窗口句柄
static BOOL flag=TRUE;
static HMENU hWndMenu=NULL; //菜单句柄
COMMONDICT word[COMMONNUM]= //字典,结构定义在头
//文件中
{
{"&File", "文件&F"},
{"&New", "新建&N"},
{"&Open", "打开&O"},
{"&Save", "保存&S"},
{"Save &As", "另存为&A"},
{"P&rinter Setup", "打印机设置&R"},
{"Page Set&up", "页面设置&U"},
{"&Print", "打印&P"},
{"E&xit", "退出&X"},
{"&Edit", "编辑&E"},
{"&Undo", "撤消&U"},
{"&Redo", "重做&R"},
{"Cu&t", "剪切&T"},
{"&Copy", "复制&C"},
{"&Paste", "粘贴&P"},
{"&Window", "窗口&W"},
{"&Windows", "窗口&W"},
{"&Tile", "级联&T"},
{"&Cascade", "层叠&C"},
{"&Hide", "隐藏&H"},
{"&Hidden", "隐藏&H"},
{"&View", "展示&V"},
{"&All", "所有的&A"},
{"&Hidden Document List", "隐藏所有的文本窗口&H"},
{“&Help","帮助&H"},
{"Help Topics","帮助主题&W"},
{"&What's This?","这是什么&W"},
{"&About", "关于&A"},
};
#pragma data_seg( ) //共享段声明结束
BOOL WINAPI _export InstallMsgFilterHook(HWND hWnd);
LRESULT CALLBACK _export MsgFilter(int nCode ,WPARAM wParam,LPARAM lParam); //钩子类型声明
BOOL WINAPI searchDict1(char * english,char * chinese);//查字典
BOOL WINAPI DllEntryPoint(HINSTANCE hInThis,DWORD fDwReason,LPVOID lpvResvered)
{
hInstance=hInThis;
switch( fDwReason)
{……}//参见相关文档
return (TRUE);
}
BOOL WINAPI _export InstallMsgFilterHook(HWND hWnd)
{//安装钩子函数
DWORD pid;//线程ID
shihWnd=hWnd; //被挂接的程序窗口
hWndMenu=GetMenu(shihWnd);//获取菜单句柄
if(!hWndMenu)MessageBox(NULL,"Get Menu Faild",
"Install",MB_OK);
return(MsgFilterHook=SetWindowsHookEx(WH_GETMESSAGE ,(HOOKPROC)MsgFilter,hInstance ,GetWindowThreadProcessId(hWnd,&pid) )!=NULL);//安装指定线程的钩子函数
}
LRESULT CALLBACK _export MsgFilter(int nCode ,WPARAM wParam,LPARAM lParam)
{//消息过滤函数
HMENU hPopup; //主菜单句柄
char lpMenustr[30];//菜单项的字符串
int i,Count;
if(nCode>=0) //小于零应留给系统处理
{
switch(nCode)
{
MSG * msg;
HMENU hSubMenu,hSub2Menu;//两层菜单
HWND hWndThis;
char buf[100],chinese[100];//字符缓冲区
int topnum,subnum,sub2num;//分别为顶级菜单项数、
//弹出菜单项数、二级子菜单项数
int i=0,j=0,k=0;
UINT ItemID; //菜单项ID,修改菜单项时不可更改菜单的命令ID
MENUITEMINFO MenuItem;
case HC_ACTION :
msg=(MSG *)lParam;
switch (msg->message)
{
case WM_COMMAND:
if(msg->wParam==CM_MYMENUITEM)//是自定义菜单项吗
{
hWndThis=msg->hwnd;//获取的窗口句柄
hWndMenu=GetMenu(hWndThis);//获取主菜单句柄
topnum=GetMenuItemCount(hWndMenu);//获取顶级菜单项数
for(i=0;i<topnum-1;i++)
{
if(GetMenuString(hWndMenu,i,buf,100,MF_BYPOSITION))
//获取指定位置菜单项的字符串
{
if(searchDict1(buf,chinese))//若从字典中查到
{
ItemID=GetMenuItemID(hWndMenu,i);//先获取菜单ID
ModifyMenu(hWndMenu,i,MF_STRING|MF_BYPOSITION,ItemID,chinese);//修改菜单
DrawMenuBar(hWndThis);//立即更新
}
else
{
}
}
}
if (hWndMenu)
{
for(i=0;i<topnum-1;i++)
{//汉化弹出菜单项
hSubMenu=GetSubMenu(hWndMenu,i);
subnum=GetMenuItemCount(hSubMenu);
for(j=0;j<=subnum;j++)
{
{
if(GetMenuString(hSubMenu,j,buf,100,MF_BYPOSITION))
//弹出菜单项内容是字符吗
{
if(searchDict1(buf,chinese))//查字典
{
ItemID=GetMenuItemID(hSubMenu,j);
if(ItemID==0xFFFFFFFF)//还有下层子菜单吗
{//汉化
hSub2Menu=GetSubMenu(hSubMenu,j);
sub2num=GetMenuItemCount(hSub2Menu);
for(k=0;k<sub2num;k++)
{
if(GetMenuString(hSub2Menu,k,buf,100,MF_BYPOSITION))
{
if(searchDict1(buf,chinese))
{
ItemID=GetMenuItemID(hSub2Menu,k);
ModifyMenu(hSub2Menu,k,MF_STRING|MF_BYPOSITION,ItemID,chinese);
}
}
}
}
ModifyMenu(hSubMenu,j,MF_STRING|MF_BYPOSITION,ItemID,chinese);
DrawMenuBar(hWndThis);//立即更新
}
}
}
}
}
}
else
MessageBox(NULL,"hWndMenu is NULL","My hook",MB_OK);
}//未获得主菜单句柄
break;
default:
break;
}
break;
}
}
return( CallNextHookEx(MsgFilterHook,nCode,wParam,lParam));
}
BOOL WINAPI searchDict1(char * english,char * chinese)
{
char in[100],out[100];
int i;
BOOL flag;//查找标志
lstrcpy(in,english);
for(i=0;i<60/*NUMBER*/;i++) //此处限定字典的长度
{
lstrcpy(out,word[i].English);
flag=lstrcmp(in,out);
if(!flag)//查到
{
lstrcpy(chinese,word[i].Chinese);//复制中文字符
break;
}
else
{//此处可添加查第二字典的代码
}
}
return(!flag); //返回查找结果
}