IE和Outlook Express的翻译插件设计
摘要:本文主要介绍了在IE和Outlook Express中加入翻译插件的设计过程。对于IE,插件开发主要采用创建BHO对象,浏览器调用过程中载入COM组件DLL文件来实现,翻译部分涉及到两部分:一是网页HTML源码文本的分析提取,二是调用翻译引擎生成包含译文的目标HTML文件。对于Outlook Express,本文采用的是程序注入式方式的插件开发,采用了Nektra公司的OEAPI库函数编写翻译插件,翻译部分也涉及到两部分:一是OE的邮件格式解码处理,二是调用翻译引擎生成包含译文的目标邮件。
关键词:翻译插件,IE,Outlook Express,HTML分析,邮件解码
Plug-ins Design For IE & Outlook Express
Abstract: This paper introduces how to plug the translate-plug-in into IE & Outlook Express. For IE, the plug-in mainly depends on the creating of the BHO object and loading the DLL file of COM component during the IE launching. Translation procedure refers to two parts: the analyses of HTML source file and the calling of translation engine to build the target HTML file. For Outlook Express, the plug-in uses the program injection, coding with the library of OEAPI, Nektra Corporation. Translation procedure also refers to two parts: the decoding of OE email format and the calling of translation engine to build the target email file.
Key Words: Translate-Plug-in, IE, Outlook Express, HTML analyze, Email decode
目录
一、引言
二、IE浏览器插件开发
2.1 IE插件开发方法介绍
2.2 IE翻译插件的设计
2.3 IE翻译插件的实现
2.3.1 与IE进行COM通信的主要函数
2.3.2 网页内容翻译的实现
2.3.3 注册表项修改
三、Outlook Express插件开发
3.1 OEAPI介绍
3.2 Outlook Express翻译插件设计
3.3 Outlook Express翻译插件实现
3.3.1 OEAPI中重载函数的实现
3.3.2 邮件格式分析
四、实现效果
五、总结
参考文献
致谢
因特网(Internet)的快速发展极大地加速了全球范围内的信息交换,只需借助一个Web浏览器,人们就能轻松方便地获得世界各地的数据和信息。在人们享用Internet所带来的巨大便利时,常会遇到一个语言障碍问题,世界各国有各自不同的语言,Internet上由不同国家提供的Web页面包含了不同种类的语言文字,当人们没能掌握某种语言时,也就无法理解该语言的Web页面。因此,对网上的页面进行翻译就显得十分必要。
上网人们最长用到的两个功能就是网页浏览和电子邮件收发。而这其中又以Microsoft公司的Windows操作系统自带的Internet Explorer(简称IE)和Outlook Express(简称OE)最为流行。所以在这两款软件基础上开发的插件能够拥有较多的用户群体。
针对IE和OE开发的插件是目前比较热门的技术。本文主要针对IE和OE的翻译插件设计分别加以讨论研究,给出其设计思想和框架结构,然后对实现上的细节进行分析,最后给出实现效果。
目前有许多技术、工具可以用来定制IE浏览器,从IE3.0开始提供WebBrowser control,从IE4.0开始提供Browser Helper Objects(BHOs),这些都是让开发人员用来定制IE的。从IE5.0开始支持增加工具条按钮和菜单项,以及右键点击弹出的菜单项(context menu)。目前定制IE6.0浏览器设计四种元素:COM组件,HTML,Script文件,注册表。向注册表里添加一些Keys和Values,写一个特定的COM组件或脚本文件,就可以定制IE界面了。
基本上,与IE相关的COM对象有四种类型:BHOs,带定制命令的BHOs,command-only objects, Explorer deskband。BHO是一个简单的COM服务器,只需实现IObjectWithSite接口即可。BHOs并不和用户界面相连,它只是在浏览器启动时起作用。BHOs会收到一个指向浏览器IUnknown接口的指针。
如果要往浏览器里添加命令,那么在写BHOs或COM组件时需要实现IOleCommandTarget接口,IE通过这个接口来实现和组件交互并执行某项功能。这种组件需要和一个菜单项或一个工具条按钮联系起来工作。
COM是面对对象的软件模型,接口是一组逻辑上相关的函数集合。在COM标准中,一个组件程序也被称为一个模块,它可以是一个动态连接库(DLL), 被称为进程内组件(in-of-process component)也可以是一个可执行程序(EXE),被称为进程外组件(out-of-process component)。OLE技术以COM规范为基础,OLE充分发挥了COM标准的优势,使Windows操作系统上的应用程序具有极强的可交互性。[1]
要往IE工具条上添加按钮,需要向注册表里添加条目,内容包括使用何种图标,显示文字内容,点击按钮时所作的动作等。每个工具条有其对应的菜单项,可以在注册表中加以说明。定义一个新按钮需要两种信息:配置和动作设定。所有条目都是字符串(REG_SZ类型)。如果Default Visible设为No,则该按钮不会出现在工具条上,不过在工具条的定制对话框中可以看到。如果用户先前定制过工具条,则无论Default Visible的值是什么,按钮都不会自动出现在工具条上。
Clsid条目的值是一个GUID,这里使用的是IE浏览器默认工具条的值{1FBA04EE-3024-11D2-8F1F-0000F87ABD16}。按钮的动作设定有三种选择:运行一个可执行文件,打开一个脚本或HTML页面,或者引发一条命令,该命令定义在某个实现了IOleCommandTarget接口的COM组件中。如果要运行一个可执行文件,需要在注册表中添加一个名字为Exec的value,内容为可执行文件的全路径名,但是不能指定命令行参数。
如果要执行一个VB脚本或java脚本,仍是用Exec,如果要打开一个HTML页面,value的名字要改用Script。两种情况的区别是:Exec意味着指定的文件是由它的关联程序所执行的。如果在Exec中指定一个HTML文件,则会启动默认浏览器去浏览该页面。如果在Exec中指定一个.vbs或.js脚本文件,那么只是简单的执行其中的代码。脚本文件作为单独可执行文件运行时,不能得到当前浏览器的实例和由当前正被浏览的文档所暴露出来的对象模型。如果想使用这些对象,需要写一个HTML文件,其中仅包含脚本代码,并用Script来执行。[2]
若把IE工具条按钮连接到一个COM对象上,添加的value名字为ClsidExtension,其内容是实现了IOleCommandTarget接口的COM对象的CLSID。
IObjectWithSite接口
一个场所(site)是一个中间对象,它位于容器对象和被包容对象之间。通过它,容器对象管理被包容对象的内容,也因此使得对象的内部功能可用。因此,容器方要实现接口IOleClientSite,被包容对象要实现接口IOleObject。通过调用IOleObject提供的方法,容器对象使得被包容对象清楚地了解其HOST的环境。如果容器对象是IE,则被包容对象只需实现一个轻型的IObjectWithStie接口。该接口提供了一下两个方法:SetSite和GetSite。SetSite是用来接收IE浏览器的IUnknown指针,保存该指针以备将来使用。GetSite是从通过SetSite方法设置的场所中接收并返回指定的接口,用来查询前面保存的接口指针以进一步取得指定的接口。插件制作的关键是存取IE的浏览机制。 [3]
IOleCommandTarget接口
IoleCommandTarget接口提供了QueryStatus和Exec方法,QueryStatus方法主要用于实现查询一个OLE对象,返回采用IOLECommandTarget接口命令的状态。Exec方法用于接受IE扩展菜单命令。QueryStatus方法会被IE调用来获得当前菜单的状态,我们需要返回 OLECMDF_ENABLED或者其它值来表示是否允许点击。Exec()函数则是功能的实现。
IE翻译插件主要功能是:
(1)点击翻译按钮后调用翻译程序对当前页面进行翻译,结果在新页面中显示,系统应能对原文页面作出实时快速的翻译,有多种语言翻译能力,系统一般针对特定的语言使用者,所以更多的情况是多种源语言到一种目标语言(用户语言)的翻译。
(2)原文页面和译文页面的对照显示功能。系统应能支持原文页面和翻译结果页面(以下简称译文页面)的对照显示,并且译文页面应在风格上与原文页面保持一致:包括页面布局的一致和图象、动画等页面元素的正常显示,同时译文页面要实现与原文页面一样的数据交互和链接导航等功能。根据上面插件常用方法介绍来看,这里采用的是在IE默认工具条上添加一个按钮,按钮点击后引发一条命令,由COM组件来执行。基本执行过程如下图1:
图1 IE翻译插件框架设计
根据翻译插件的基本要求,本文设计开发了一个基于Windows平台和IE的Internet翻译插件。插件利用Visual C++ 6.0的ATL库进行编写,通过IE调用COM组件的执行部分DLL文件来实现基本功能。
VC6.0建立一个ATL工程,这里向导已经使类从接口IObjectWithSiteImpl继承,这是一个ATL模板类,它提供了接口IObjectWithSite的基本实现。一般情况下,没有必要重载成员函数GetSite()。不过SetSite()实现代码需要加以定制。在translate.h中添加支持的接口IOleCommandTarget。
(1)SetSite()方法
这是COM对象被初始化的地方,在这里要取得指向浏览器的指针。这里就是IWebBrowser2的指针。利用ATL灵敏指针先从IUnknown取得IServiceProvide,再利用QueryService()取得 IWebBrowser2。获得了IWebBrowser2指针后,下面的工作就好做了。这块的代码如下表1:
STDMETHODIMP Ctranslate::SetSite(IUnknown *pUnkSite)
{
if (pUnkSite != NULL)
{
// 检索并存储 IWebBrowser2 指针
CComQIPtr sp = pUnkSite;
HRESULT hr = sp->QueryService(IID_IWebBrowserApp,
IID_IWebBrowser2, (void**)&m_spWebBrowser);
}
else
{
// 释放指针
m_spWebBrowser.Release();
/*m_spTarget.Release();*/
}
// 返回
return IObjectWithSiteImpl::SetSite(pUnkSite);
}
表1:SetSite()函数部分代码
(2)Exec()函数
这块是主要功能实现的部分。这里首先调用GenereateReport()这个自定义的函数来进行网页HTML源码分析,并将分析后的HTML写入c:/temp.html临时文件下,接着调用IWebBrowser2指针的Navigate函数来在新窗口中显示结果页面temp.html。这块的代码如下表2:
STDMETHODIMP Ctranslate::Exec(const GUID *pguidCmdGroup,
DWORD nCmdID,
DWORD nCmdExecOpt,
VARIANTARG *pvaIn,
VARIANTARG *pvaOut)
{ //Exec函数主要是IE浏览器上按钮点击后DLL文件执行的主要部分
if (m_spWebBrowser != NULL)
{
CComBSTR oURL( "c://temp.html" );
GenerateReport(oURL);//分析网页源码
// Navigate to the report
VARIANT noArg;
noArg.vt = VT_EMPTY;
VARIANT flags;
flags.vt = VT_I4;
flags.lVal = navOpenInNewWindow;
m_spWebBrowser->Navigate(oURL, &flags, &noArg, &noArg, &noArg);
}
return S_OK;
}
表2:Exec()函数部分代码
(3)GenreateReport()函数
主要工作在Genereate()函数中。该函数有个参数BSTR filename,是用来确定结果存放的地方,也就是c:/temp.html文件的路径。 Genereate函数首先是要获取WebBrowser的文档对象。Web浏览器的文档属性返回一个指向文档对象的IDispatch接口的指针。 get_Document() 方法取得的仅仅是一个接口指针。我们要进一步确定在IDispatch 指针背后存在一个HTML文档对象[]1。注释 Internet Explorer不仅仅是一个HTML浏览器,而且还是一个ActiveX文档容器。 这样一来,难以保证当前浏览对象就是一个HTML文档。不过如果IDispatch指针真正指向一个HTML文档,查询IHTMLDocument2 接口一定成功。 IHTMLDocument2接口包装了DHTML对象模型用来展现HTML页面的所有功能。这部分的代码如下表3:
CComPtr pDisp;
HRESULT hr = m_spWebBrowser->get_Document(&pDisp);
if (FAILED(hr))
return hr;
// 确保我们取得的是一个IHTMLDocument2接口指针
// 让我们查询一下 IHTMLDocument2 接口 (使用灵敏指针)
CComQIPtr spHTML;
spHTML = pDisp;
// 抽取文档源代码
if (spHTML)
{
//获取文档内容并处理
}
else
{
//不是HTML文档
}
表3:IHTMLDocument2接口实现部分代码
如果IHTMLDocument2接口查询失败,spHTML指针将是NULL。 下面的内容是如何获取浏览器当前窗口的HTML源码。这里采用的是首先获得指向HTML源码的指针集合IHTMLElementCollection,然后通过item()函数获取IHTMLElement指针,最后把outerHTML属性内容读取到一个BSTR变量中。这部分的代码如下表4:
CComPtr m_pAll;
hr = spHTML->get_all((IHTMLElementCollection**)&m_pAll);
CComQIPtrpElement;
_variant_t varIndex=(long)0;
pDisp = NULL;
hr = m_pAll->item(varIndex, varIndex, &pDisp);
if (FAILED(hr))
return hr;
pElement = pDisp;
BSTR bstrHTMLText;
hr = pElement->get_outerHTML(&bstrHTMLText);
if (FAILED(hr))
return hr;
表4:获取HTML源码部分代码
下面的工作是字符的Unicode至ANSI的转化。需要用到宏USES_CONVERSION; BSTR(Basic STRing,Basic字符串)是一个OLECHAR*类型的带长度前缀的Unicode字符串。它被描述成一个与自动化相兼容的类型。由于操作系统提供相应的API函数(如SysAllocString)来管理它以及一些默认的调度代码,因此BSTR实际上就是一个COM字符串,但它却在自动化技术以外的多种场合下得到广泛使用。
LPSTR和LPWSTR是Win32和VC++所使用的一种字符串数据类型。LPSTR被定义成是一个指向以NULL(‘/0’)结尾的8位ANSI字符数组指针,而LPWSTR是一个指向以NULL结尾的16位双字节字符数组指针。这里需要把BSTR转化成LPSTR,因为后来对于字符串中单个字符的操作都是基于TCHAR的,而且翻译引擎的参数和结果都是char*类型的。这里的TCHAR是在编译时候通过编译参数来决定其数据类型的。这部分的代码如下表5:
LPTSTR psz, p, t;
p = new TCHAR[SysStringLen(bstrHTMLText)];
psz = new TCHAR[SysStringLen(bstrHTMLText)];
lstrcpy(psz, OLE2T(bstrHTMLText));
SysFreeString(bstrHTMLText);
表5:字符串转换部分代码
这里psz就是HTML源码的LPSTR字符串,p是用于提取文本的临时字符串。
(1)HTML源码分析
主要是对HTML源码中文本部分的提取和翻译工作。其实现部分是在GenerateReport()函数中。提取主要采用while循环遍历扫描的方式。主要查找'<'和'>'tag外的部分。这块的代码如下表6:
int i = 0, j = 0, k = 0, tIndex = 0;
while(i < lstrlen(src))
//while循环遍历HTML源码,提取文字部分,调用翻译引擎,替换翻译部分
{
if(src[i] == '<')//寻找左括号部分
{
while(src[i] != '>')//寻找右括号部分
target[tIndex++] = src[i++];
continue;
}
target[tIndex++] = src[i++];
j = i;//记录文字部分的起始位置
while(src[j++] != '<')//寻找文字部分结束位置 ;
j --;//位置修正
j --;
//跳过文字开头部分的空格和回车
while(src[i] == (char)(161) || src[i] == (char)(32)
|| src[i] == (char)(13) || src[i] == (char)(10))
target[tIndex++] = src[i++];
//跳过文字结尾部分的空格和回车
while(src[j] == (char)(161) || src[j] == (char)(32)
|| src[j] == (char)(13) || src[j] == (char)(10))
j--;
int len = j - i + 1;//文字部分长度
if (len <= 0)//长度小于零不处理,即文字部分全部是空格或回车
continue;
for(k = 0; k < len; k++)//复制文字部分到p字符串
temp1[k] = src[i + k];
temp1[k] = '/0';//结尾加结束符
//每一个p字符串即提取出的文字部分.
//调用翻译引擎翻译,这里暂定结果为修改后的字符串p
GetTranslation(temp1, temp2, MAXLEN, 1);
len = lstrlen(temp2);
for(k = 0; k < len; k++)//文本替换
target[tIndex++] = temp2[k];
i = j + 1;
}
表6:HTML源码分析部分代码
(2)调用翻译引擎
翻译部分就是调用翻译引擎jcmt_transent (),其参数是每次提取出的文本字符串,引擎将字符串翻译后,将HTML源码中对应部分替换掉。这块的代码如下表7:
void Ctranslate:: GetTranslation(char *src, char * target, int sizeoftarget, int code)
{//翻译引擎在这里可以调用,这里简单的用'*'来替换原有文本。
jcmt_transent(src, target, MAXSIZE, GBK);
}
表7:调用翻译引擎部分代码
(3) 结果写入文件
最后将HTML源码写入temp.html文件中。这块的代码如下表8:
HANDLE handle;
DWORD Num;
handle = ::CreateFile("C://temp.html",
GENERIC_READ|GENERIC_WRITE, 0, NULL, CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL, NULL);
if(INVALID_HANDLE_VALUE == handle )
return S_OK;
WriteFile(handle, psz, sizeof(char)*strlen(psz), &Num, NULL);
//结果写入临时存储页面
CloseHandle(handle);
表8:结果写入文件部分代码
主要是以下三步:
1、创建按钮的GUID(globally unique identifier) 可以通过Visual Studio中的Guidgen.exe来产生GUID,我生成的按钮ID为{DCEAAE91-7FE5-477e-B1C8-48D1FC4C6AA0}
2、注册表编辑器创建创建子键 HKEY_LOCAL_MACHINE/SOFTWARE/Microsoft/Internet Explorer/Extensions/{DCEAAE91-7FE5-477e-B1C8-48D1FC4C6AA0}
3、在此子键下创建如下字串值。
(1)CLSID 这是IE默认工具条的CLSID,其值必须为{1FBA04EE-3024-11D2-8F1F-0000F87ABD16}
(2)Default Visible 指明此按钮默认状态下是否可见,Yes表示可见,No为不可见。注:如果工具栏已被自定义调整过,则新创建的按钮即使设置默认可见也不会显示在工具栏,而是在自定义工具栏对话框里出现。可以选择重置工具栏使其出现。
(3)ButtonText 按钮文字,这里设为“IEPlugin”
(4)Icon 默认状态下的图标全路径,例如c:/favcion.ico。也可以是EXE文件中包含的图标,例如:C:/PROGRA~1/NETANTS/NetAnts.exe,1000,其中1000为图标的ID资源编号。这里将按钮的图标集成到IEPlugin.dll文件内,资源号为201。
(5)HotIcon 鼠标移到按钮上时的图标全路径,即为DLL文件的全路径。
这里我采用了VC6.0工程内的translate.rgs文件来执行注册表项添加工作,这里的语法和REG脚本略有不同,其中%MODULE%代表当前DLL存放的路径。实现代码如下表9:
HKLM
{
SOFTWARE
{
Microsoft
{
'Internet Explorer'
{
Extensions
{
ForceRemove '{4B11F6CE-155A-4B90-BC41-58F9EB3C3DFF}'
{
val ButtonText = s 'IEPlugin'
val CLSID = s '{1FBA04EE-3024-11d2-8F1F-0000F87ABD16}'
val ClsidExtension = s '{4B11F6CE-155A-4B90-BC41-58F9EB3C3DFF}'
val 'Default Visible' = s 'Yes'
val HotIcon = s '%MODULE%,201'
val Icon = s '%MODULE%,201'
val MenuText = s 'IEPlugin'
}
}
}
}
}
}
表9:注册表项修改部分代码
由于Outlook Express是Windows系统自带自带的邮件收发工具,它并不像微软的Office系列中的Outlook一样有其公开的COM模型,所以其插件的开发并不能像Office差将一样实现标准的COM接口那样容易。目前大部分OE插件的开发采用的是程序注入方式开发,其基本思路是顶级窗口(进程主窗口)创建、注销等事件时收到消息判断窗口是否 OE 主窗口,然后遍历 OE 的子窗口,找到工具栏窗口句柄,通过改变窗口消息处理的过程来完成相关动作。这样基本是可以完成插件开发工作的。但是实现起来的难度比较大,需要对Windows SDK编程,窗口钩子程序,COM服务,RemoteThread等相关知识比较熟悉。
正因为OE直接开发比较困难,不少软件公司研究开发了自己的OE开发库,通过这些库函数再来对OE进行插件开发就显得容易许多。
Nektra公司开发出了一套良好的OE库函数OEAPI,其标准版现在授权是免费的,完整企业版则有30天的试用期。通过这个库函数开发者可以定制OE的界面,操纵OE传递的消息,且库函数采用COM标准编写,支持多种开发语言。通过其开发包内部自带的Demo演示程序用户可以了解到其强大的功能。
开发包里包含四个相关的DLL文件,一些示例工程和帮助手册。该插件工作的机制大体如下:开机启动自动载入launcher.exe程序,该程序调用oehook.dll来检测OE是否启动。一旦OE程序启动,OEAPI会调用已注册的插件嵌入OE中运行。研究了相关的资料后,我发现这个API基本能够满足这里插件制作的需要。可以用VC建立一个ATL工程,然后调用OEAPI,获得OE默认工具栏的句柄ID,添加插件按钮。在有邮件选中事件发生后激活插件按钮。点击按钮后获取邮件内容,调用翻译引擎翻译内容后结果在OE新窗口中显示。主要COM组件介绍如下表10:
COM
组件
|
描述
|
oeapiinitcom.dll
|
包括了OEAPIINITCOM类库。他包含了OEAPIInit ActiveX对象,该对象能够在OE启动和关闭的过程中分别触发OnInitOEAPI和OnShutdownOEAPI事件。在这两个事件之间开发人员可以实例化任何其所需要的OEAPI类库中的对象。
|
oecom.dll
|
这个DLL运行在OE的内存地址空间内并且必须由oehook.dl在OE启动的时候载入。
|
oestore.dll
|
包括了OESTORE类库。他可以让你实例化OEFolderManager这个主要的文件夹和消息操作对象。
|
oehook.dll
|
这个DLL必须在开机时候由launcher.exe程序启动。他会在OE启动的时候载入oecom.dll。该DLL实现了窗口钩子功能。
|
launcher.exe
|
这个程序主要是在开机时启动并载入oehook.dll文件。
|
OEAPI 3.2.1 Enterprise Evaluation Version其试用期信息是记录在系统注册表下列项: HKEY_CURRENT_USER/Software/Nektra/ 在HKEY_CURRENT_USER/Software/Nektra/OEAPI/Data目录下有Data和Flags两个REG_BINARY二进制值,其意义Flags代表该软件安装时的时间,Data代表上次使用时的时间。这里时间是经过加密处理过的64位二进制位,如果Data和Flags相同,则表示软件还有30天剩余试用期,若系统修改时间,时间修改滞后,此时调用OE则Data修改为现有系统时间,试用时间减少。但是如果系统修改时间提前,调用OE后程序会判断时间不正常,提示试用期到期。注册表只负责记录程序初次运行时间和最近一次运行时间提供参考。如果该Nektra目录不存在,则试用期信息清零,剩余时间从下一次OEAPI被调用时算起。所以这里OE插件的开发采用了OEAPI库函数来开发。同时由于标准版有功能限制,这里暂且使用完整企业版。
OE翻译插件主要功能是:(1)点击翻译按钮后调用翻译插件对当前邮件进行翻译,结果在新窗口中显示,系统应能对原文页面作出实时快速的翻译,有多种语言翻译能力,系统一般针对特定的语言使用者,所以更多的情况是多种源语言到一种目标语言(用户语言)的翻译。(2)原文页面和译文页面的对照显示功能。系统应能支持原文页面和翻译结果页面(以下简称译文页面)的对照显示,并且译文页面应在风格上与原文页面保持一致:包括页面布局的一致和图象、动画等页面元素的正常显示,同时译文页面要实现与原文页面一样的数据交互和链接导航等功能。主要体系结构如图所示:
图2:OE翻译插件框架结构图
根据翻译插件的基本要求,本文设计开发了一个基于Windows平台和Outlook Express的Internet翻译插件。插件利用Visual C++ 6.0的ATL库和Nektra公司的OEAPI库进行编写,通过oehook.dll检测到OE启动,载入注册过的COM组件来实现基本功能。 VC6.0建立一个ATL工程,ATL向导已经集成了一些接口。这里还需要在atl_addin.h中添加支持的OEAPI相关功能的接口,这里用IdispEventSimpleImpl接口来实现。其中需要重载的函数需要在添加到消息映射机制中。接下来的工作主要是三个需要重载的函数编写。
(1)OnInitOEAPI()实现
该函数主要是在OE启动时执行相关动作。随着OE的启动,相关插件中的oeapi对象即可以进行实例化工作。主要是IOEAPIObj和IOEFolderManager这两个对象,前者主要负责OE整体的一些动作执行和消息处理,后者则是对OE文件夹的操作,这里添加了一个名为“翻译结果”的新文件夹。这块的主要代码如下表11:
//初始化OEAPI函数
STDMETHODIMP Catl_addin::OnInitOEAPI()
{
if(SUCCEEDED(m_oeapi.CoCreateInstance(CLSID_OEAPIObj)))
{
// sink events
_OEAPIObjEvents::DispEventAdvise((IUnknown*)m_oeapi,
&__uuidof(OEAPI::IOEAPIObjEvents));
//进行OE界面中需要的组件实例化添加工作
}
if(SUCCEEDED(fm.CoCreateInstance(CLSID_OEFolderManager)))
{
//在OE中新建一个文件夹“翻译结果”
result_foldid = fm->CreateFolder(1, "翻译结果");
}
return S_OK;
}
表11:OEAPI函数部分代码
(2)工具栏按钮添加
这块也是在OnInitOEAPI()函数体内实现。首先是设定按钮的图片路径,接下来是获取指向OE默认工具栏的指针,然后调用CreateButton()函数来创建新按钮。这块代码如下表12:
// create a toolbar
CComPtr toolbar;
toolbar = m_oeapi->GetOEToolbar();
if(toolbar != NULL)
{
toolbar->SetLargeButtons(FALSE);
m_toolbarId = toolbar->GetID();
// create a botton
CComPtr button;
button = toolbar->CreateButton(_T("Plugin TEST"), normal.c_str(), over.c_str());
if(button != NULL)
m_button = button->GetID();
}
表12:工具栏按钮添加部分代码
这样插件需要的按钮就出现在OE默认工具栏上了。下面的工作是实现按钮点击后的动作。
(3)OnToolbarButtonClicked()实现
这个函数是响应按钮点击后执行,按钮需要实现的功能可以在这里实现。这里首先判断被点击的按钮是不是插件按钮,这可以通过判断按钮的id来实现。接下来就是需要实现插件的基本功能,就是对所选中的邮件提取MIME源码进行翻译,结果存放在翻译结果中,并在新窗口中显示。这个功能分几步走:首先获取所选邮件的编号id,如果id为-1则表明没有邮件被选中,这时程序选择弹出对话框提示错误。如果有邮件被选中,下面就需要获取邮件的源码。首先是获得当前邮件所在文件夹的编号,接着调用文件夹对象中的OEGetMessage()函数获取邮件对象IOEMessage。继续通过IOEMessage对象中的GetAllSource()函数就可以获取BSTR字符串类型的邮件MIME源码。获取的源码经过Analyse()函数进行分析,调用翻译引擎进行翻译,重新生成翻译后的MIME源码字符串作为Analyse()函数的返回值。最后将当前文件夹定位到“翻译结果”文件夹,创建新邮件,其内容为Analyse()函数的返回值,即翻译结果邮件,将该邮件设为当前邮件,再调用OpenCurrentMessage()来在新窗口中显示结果。这块的代码如下表13:
STDMETHODIMP Catl_addin::OnToolbarButtonClicked(long toolbarId, long buttonId)
{
if(m_toolbarId == toolbarId && buttonId == m_button)
{
//获得选中的邮件的ID
long id = m_oeapi->GetFirstSelectedMessageID();
if(-1 == id)
{
MessageBoxA(NULL, "请选择要翻译的邮件", "ERROR!", MB_OK);
return S_FALSE;
}
CComPtr folder;
CComPtr msg;
long fid;
fid = m_oeapi->GetSelectedFolderID();//获得当前文件夹ID
folder = fm->GetFolder(fid);
msg = folder->OEGetMessage(id);//获取邮件的源码
/*****************************************/
/*该块是对源码进行分析部分 */
/*****************************************/
msg = folder->CreateMessage(re, TRUE);//在结果文件夹中添加翻译结果邮件
m_oeapi->SetSelectedFolderID(result_foldid);
m_oeapi->OpenCurrentMessage();//新窗口显示结果文件
}
return S_OK;
}
表13:OnToolbarButtonClicked()函数部分代码
(4)OnShutdownOEAPI()实现
该函数主要是在OE关闭时处理一些对象的删除和停止事件的监听。这块的代码如下表14:
STDMETHODIMP Catl_addin::OnShutdownOEAPI()
{
OutputDebugString(_T("Catl_addin::OnShutdownOEAPI/n"));
// stop listening oeapi events
if(m_oeapi != NULL)
{
_OEAPIObjEvents::DispEventUnadvise((IUnknown*)m_oeapi,
&__uuidof(OEAPI::IOEAPIObjEvents));
m_oeapi = NULL;
}
// stop listening init events
_OEAPIInitEvents::DispEventUnadvise((IUnknown*)m_init,
&__uuidof(OEAPIINITCOM::IOEInitEvents));
return S_OK;
}
表14:OnShutdownOEAPI()函数部分代码
(5)注册表项修改
插件制作完成后,还需要在注册表Nektra公司的OEAPI对应项下添加相关条目后,才能在oehook.dll检测到OE启动的时候载入。这块的实现是在atl_addin.rgs中实现。这块的代码如下表15:
HKCU
{
Software
{
Nektra
{
OEAPI
{
Plugins
{
val OEPlugin = s 'OEPlugin.atl_addin'
}
}
}
}
}
表15:注册表项修改部分代码
这部分三块,一块是在OnToolbarButtonClicked()函数中实现,主要是对邮件头部源码的生成和对翻译部分源码的添加整合。这块的代码如下表16:
fid = m_oeapi->GetSelectedFolderID();//获得当前文件夹ID
folder = fm->GetFolder(fid);
msg = folder->OEGetMessage(id);//获取邮件的源码
folder = fm->GetFolder(result_foldid);
priCon = "text";
secCon = "plain";
bodyHandle = msg->GetPlainBody();
header = msg->GetHeader();
source = GetHeaderSource(header);
source += Analyse(msg->GetSubject());
body = Analyse(msg->GetBodyText(bodyHandle));//对文本部分进行分析
source += "/n/n";
source += body;//形成整体的邮件源码
表16:邮件格式分析部分代码
第二块是在GetHeaderSource()函数下进行,主要是对邮件中头部源码有用信息的提取。这块代码如下表17:
_bstr_t Catl_addin::GetHeaderSource(_bstr_t header)
{
USES_CONVERSION;
LPTSTR psz, temp, prop, test;
psz = new TCHAR[SysStringLen(header)];
temp = new TCHAR[SysStringLen(header)];
prop = new TCHAR[300];
psz = _com_util::ConvertBSTRToString(header);
int len = lstrlen(psz), i = 0, index, count;
temp[0] = '/0';
count = 0;
while(i < len && count < 2)
{
index = 0;
while(psz[i] != ':')
{
prop[index++] = psz[i];
i++;
}
prop[index] = '/0';
if(!lstrcmp(prop, "From") || !lstrcmp(prop, "To"))
{
lstrcat(temp, prop);
index = strlen(temp);
while(!(psz[i] == 0x0d && psz[i+1] == 0x0a
&& !(psz[i+2] == 0x20 || psz[i+2] == 0x09) ))
{
temp[index++] = psz[i];
i++;
}
temp[index++] = 0x0d;
temp[index++] = 0x0a;
temp[index] = '/0';
count++;
}
while(!(psz[i] == 0x0d && psz[i+1] == 0x0a
&& !(psz[i+2] == 0x20 || psz[i+2] == 0x09) ))
i++;
i +=2;
}
header = temp; //生成邮件的头部源码
header += "Subject: ";
return header;
}
表17:GetHeaderSource()函数部分实现代码
另一块工作主要是在Analyse()函数下进行,是对文本部分调用翻译引擎翻译,并重新生成翻译后的MIME源码。这块的代码如下表18:
_bstr_t Catl_addin::Analyse(_bstr_t source)
{
//这里是调用翻译引擎对文本进行翻译的部分
USES_CONVERSION;
LPTSTR src, target;
src = new TCHAR[SysStringLen(source)];
target = new TCHAR[SysStringLen(source) * 2];
src = _com_util::ConvertBSTRToString(source);
//原来是用lstrcpy(psz, OLE2T(str)); OLE2T这里会出错,原因不明。
jcmt_transent(src, target, SysStringLen(source) * 2, GBK);
source = _com_util::ConvertStringToBSTR(target);
return source;
}
表18:翻译引擎调用部分代码
测试环境为Windows XP SP2 和Internet Exploerer 6.0, Outlook Express 6.0。两款翻译插件的安装程序是用NSIS脚本编写制作完成的。
(1)IE翻译插件
双击IE翻译插件安装程序.exe文件,执行插件安装程序。安装步骤如下列图所示:
接着可以打开IE浏览器,在地址栏输入 雅虎日本的网址 http://www.yahoo.co.jp/index.html ,
原始页面如图3:
图3:原始网页截图
点击按钮翻译后的页面如图4:
图4:翻译后的网页截图
IE翻译插件的卸载可以在 开始菜单->所有程序->IE翻译插件->Uninstall 部分找到。卸载程序的步骤如下列图所示:
(2) Outlook Express翻译插件效果
双击Outlook Express翻译插件安装程序,安装步骤如下列图所示:
安装完成后,启动Outlook Express,如图5:
图5:启动后的Outlook Express
点击翻译按钮后的界面如图6:翻译结果窗口如图7:
图7:Outlook Express翻译窗口显示
Outlook Express翻译插件的删除可以在 开始菜单->所有程序->Outlook Express翻译插件->Uninstall 部分找到。卸载程序的步骤如下列图所示:
IE翻译插件的基本功能是实现了,不过这里还有许多需要改进的地方。对于HTML源码的分析可以添加更多的元素识别,插件程序的鲁棒性还需要加以改进,这些是今后需要完成的目标。
OE翻译插件基本实现了相关功能,但是还有很多不完善的地方。OE翻译插件的按钮目前只实现了在主窗口的显示,消息子窗口由于比较复杂,暂不支持该功能。在今后的工作中我会加以改进。
IE和Outlook Express翻译插件的制作对于熟练的人来说不难,但是对于不了解的人也不容易。在准备和开发的过程中,自己遇到了许许多多的困难和挫折,查找了大量的资料,阅读了许多文章,从好多前人的成果和经历中我能找到自己问题的答案,整个过程也是自己对未知领域的学习和探索。虽然最终的成果很不完美,但是每一个呈现的内容都是数十次尝试失败后归纳出的结果。对于Window编程和COM组件技术等的学习还在继续。
[1] Kent Sharkey, Creating Add-ons for Internet Explorer: Toolbar Buttons, MSDN, [ http://msdn.microsoft.com/en-us/library/bb735854(VS.85).aspx ], 2007
[2] Dino Esposito, Customizing Microsoft Internet Explorer 5.0, MSDN, 1999
[3] Dino Esposito, Browser Helper Objects: The Browser the Way You Want It, MSDN, [ http://msdn.microsoft.com/en-us/library/bb250436.aspx ], 1999
[4] Nektra Company, OEAPI Documentation, [ http://www.nektra.com/products/oeapi/doc/index.html ], 2008
[5] Pablo Yabo, Reading and Writing Messages in Outlook Express, [ http://www.codeproject.com/KB/COM/Outlook_Express_Messages.aspx ], 2004
[6] Azhisoft,使用 HOOK 实现 Outlook Express 工具条, [ http://www.cppblog.com/azhisoft/articles/8174.html ],2006
[7] Robert Kuster, Three Ways To Inject Your Code Into Another Process, [ http://www.codeguru.com/Cpp/W-P/system/processesmodules/article.php/c5767/#introduction ],2003
感谢我的父母给予我学业上的支持和鼓励。感谢我的导师尹存燕老师的悉心指导。感谢大学四年老师的辛勤培养。感谢同寝室的钱祺同学在论文写作阶段给予的帮助。