经过这个周末2天的奋战,终于完成了一个金山词霸的词库读取程序!当然,中间还看了2场欧洲杯,以及和老婆玩了几回坦克大战,否则可以更快完成吧!
做这个程序的动机很久了,因为一直在背英语单词,如果可以有一个完整的英语词库,可以完成很多事情,比如,查找一个单词的相似单词,window,widow,wind等。很自然的,这个词库就想从我们的金山词霸下手了。
首先,我去google了一下金山词霸的词库,看来没有现成的果子啊,显然词库文件也都是加密了的。所以只能自己动手写一个程序了,单元Kingsoft公司不会找我的麻烦吧!:)
在我写下这篇文章的时候,读取单词的进度已经完成了 70%左右(341500/457550个单词)。其实还是存在一些现成的英语单词词库的,在《STL Tutorial and Reference Guide》一书的示例程序中,有一个名为diction的词库,不过只有20160个单词,而现在,很快我就会有一个457550单词的词库了。
OK,下面我就把这个程序的编写过程和大家分享一下。
开发平台:Visual C++ 6.0
我的词库读取思想是:运行金山词霸2003,在程序中得到它的句柄,进而读取单词列表的子窗口句柄,然后,嘿嘿!把列表中的所有内容读取即可。It sounds good, let's go now!
1:建立了一个对话框程序,枚举桌面下的所有子窗口,也就是程序的主窗口,窗口标题中包含"金山词霸“的,即为金山词霸2003的主窗口;同样,枚举它的子窗口,窗口标题中包含”WordList“的(使用Spy++跟踪可知),即为单词列表控件。
说明:后来修改了一下,应该根据窗口ID来查找单词列表控件更好。
CString strText;
CWnd* pWndCiba=CWnd::GetDesktopWindow()->GetWindow(GW_CHILD);
while (pWndCiba) {
pWndCiba->GetWindowText(strText);
TRACE("%s [%x]\n",strText,pWndCiba->GetSafeHwnd());
if (strText.Find("金山词霸") != -1)
break;
// did not find window, get next window in list.
pWndCiba=pWndCiba->GetWindow(GW_HWNDNEXT);
}
if (pWndCiba == NULL)
return;
CWnd* pWndWordList=pWndCiba->GetWindow(GW_CHILD);
while (pWndWordList) {
pWndWordList->GetWindowText(strText);
TRACE("%s [%x]\n",strText,pWndWordList->GetSafeHwnd());
if (strText.IsEmpty())
break;
if (strText.Find("WordList") != -1)
break;
// did not find window, get next window in list.
pWndWordList=pWndWordList->GetWindow(GW_HWNDNEXT);
}
if (pWndWordList->GetSafeHwnd() == NULL) 
return;2:看来工作完成了大半,此时调用以下代码:
int nCount=pWordList->GetItemCount();
得到了返回值为457550,这就是词库的单词总数了3:问题出现了!
然后准备尝试读回列表的第一个单词,就是”A“,使用以下代码:
char pText[256];
ListView_GetItemText(pWndWordList.GetSafeHwnd(),0,0,pText,255);
结果...金山词霸崩溃,Windows崩溃!或许号称永不死机的WindowsXP没有完蛋吧,总之我只能使用键盘上的Power进行关机了,这也是我在成功前进行了多次的动作。:(4:问题在哪里呢?其实要怪自己很久没有编写进程间通信的程序了。
ListView_GetItemText()这个宏,实际上是发送LVM_GETITEMTEXT的消息,由接收消息的列表返回文本;但是,char pText[256];是在我的程序中分配的内存,怎么能由金山词霸的程序来使用呢!
后来,我还尝试了这样的方法:
HGLOBAL hGlobal=GlobalAlloc(GPTR,256);
char* pText=(char*)GlobalLock(hGlobal);
ListView_GetItemText(pWndWordList->GetSafeHwnd(),0,0,pText,255);GlobalUnlock(hGlobal);
GlobalFree(hGlobal);
其实没有解决本质的问题:GlobalAlloc()分配的内存的确可以供不同进程使用,但是应该在进程间传递hGlobal,即内存空间的句柄,然后由接收消息的程序调用GlobalLock()把hGlobal转换为本地的内存指针,显然Kingsoft公司不会给我们完成这个任务了。
如果使用其它进程共享数据的方法,比如shared memory files,都存在必须由消息接收者进行处理的问题。那么该怎么办???5:既然无法使用进程间共享数据的方法,我们换一个思路,让我们的程序在金山词霸的进程空间运行!这样就不需要考虑进程间共享数据的问题啦。非常幸运,有一种叫做DLL注入进程的方法可以实现。大家可以去google”Injecting a DLL into Another Process's Address Space“,或者在MSDN中查找LoadDll.exe,我的程序就是基于这个示例。
简单介绍LoadDll工程,因为我没有完全研究所有程序。实际上有2个工程,LoadDll,这是一个很简单的命令行程序,用法如下:
LoadDll.exe /L 3448 TestLib.dll TestFunction
/L 表示载入一个DLL
3448 表示要注入DLL的进程的ID,可以使用Task Manager得到
TestLib.dll 表示被载入的DLL
TestFunction DLL中的一个导出函数的函数名使用LoadDll.exe /U 3448 TestLib.dll 即可把DLL从进程中卸出。6:LoadDll我是根本没有改动,我修改了TestLib.dll中的TestFunction,完全重写了该函数。
首先,把步骤1中查找窗口句柄的代码拷贝过来,很不幸,因为TestLib.dll不能使用MFC,所以我只能修改为SDK的版本了。
然后,调用
ListView_GetItemText(hWndWordList,0,0,pText,255);
hWndWordList是列表窗口句柄。开始运行,大功告成了吗?
pText中返回的是”1234567890123456789012345678901234567890“!
My God,为什么?!
我尝试读取第2行,第100行,结果一样。于是我把这段代码修改一下,读取自己的一个包含列表的程序,很正常的读出了列表的内容。看来问题是出在了金山词霸的单词列表中了。在文章接近尾声的时候,读取程序终于运行完了!耗时将近1小时15分钟!没关系了,我也不打算再进行提高,只要逮住了老鼠,就是好猫嘛!7:使用Spy++再次跟踪单词列表窗口,发现拥有LVS_OWNERDATA风格,也就是一个虚列表。其实肯定是的,否则40多万行,使用普通的列表,显示速度可以和乌龟或者其兄弟蜗牛比美了。我猜想:
1)虚列表不能使用LVM_GETITEMTEXT或者LVM_GETITEM消息进行访问。这个猜想99%是错误的,因为MSDN关于虚列表不能使用的消息中没有包含这2个。限于时间,我没有对其它程序的虚列表控件进行测试了。
2)Kingsoft公司防止别人象我这么干,屏蔽了这2个消息。似乎没有其它的接口可以再进行访问了,难道对单词列表控件进行Subclass?因为我不熟悉SDK编程,也放弃了测试。因为,我决定出绝招了!使用一种非常粗鲁且无赖的方法,我,成功了!
说明:关于虚列表控件,可以在MSDN中查找“virtual list controls"或者”虚列表“。8:经过观察金山词霸的使用,发现每次在列表中选择一个单词后,在界面上方的ComboBox会显示该单词,同时右面的窗口会显示单词的解释。没错,我的方法就是去读取ComboBox中的内容,经过测试,ComboBox中的Caption就是单词了。那么怎么读取所有的单词?我们循环的选择所有的单词,就可以得到所有的单词了。代码如下:nCount=ListView_GetItemCount(hWndWordList); // get item count
nPrevIndex=ListView_GetNextItem(hWndWordList,-1,LVIS_SELECTED);
for (nIndex = 0;nIndex < nCount;nIndex++) {
if (nPrevIndex != -1) {
// un-select the previous item
ListView_SetItemState(hWndWordList,nPrevIndex,UINT(~LVIS_SELECTED & ~LVIS_FOCUSED ),LVIS_SELECTED | LVIS_FOCUSED);
}
// select the current item
ListView_SetItemState(hWndWordList,nIndex,LVIS_SELECTED | LVIS_FOCUSED ,LVIS_SELECTED | LVIS_FOCUSED);
GetWindowText(hWndCombo,pszWord,WORD_MAX);
nPrevIndex = nIndex; // save the previous indexWriteWord(nIndex,pszWord); // write the word into the file}9:最后的小结。
由于是使用改变Item的状态的方法去读取单词,大家也就可以理解为什么需要1个多小时读取40多万个单词了。当然,如果你的银子够多,换一台好机器,速度当然更快。我的爱机配置仅为:Celeron 1.7G, 256M。
关于程序的思考:
1)除了采用把DLL注入进程的方法,是否可以采取全局钩子的方法?网络上流行的星号密码读取程序就是采用这样的方法的,经过简单研究,我放弃了这种方法,不过只是因为我对hook技术不熟悉。
2)如何读取单词的解释?就是右面窗口显示的解释,使用GetWindowText()是不能成功的。似乎也可以采取无赖的手段获取的,比如发送一个Ctrl + A选择所有文本,然后Ctrl + C拷贝内容到剪贴板,接着从剪贴板读取。似乎很傻,但是看来是可以的。这个工作等到欧洲杯结束后再考虑了,如果哪位朋友有更好的意见,请不吝赐教!

解决方案 »

  1.   

    不错!怎 么不发到blog里?
      

  2.   

    祝贺你,你成功了!
    建议你把读取的单词信息写入一个文件,
    再把你这个文件的存储格式写一个说明文档(当然是方便别人使用),
    然后一起发到网(记得不要让Kingsoft公司逮着你,否着我会难过的),
    做成一个互联网上共享的词库,造福大众而功德无量!
      

  3.   

    谢谢大家的鼓励!
    关于5512的建议:建议你把读取的单词信息写入一个文件
    其实这就是程序的输出结果,每行一个单词的TXT文件,将近10M。然后我又转换成了MDB格式,好大,将近50M(经过压缩),所以大家要用的话,还是自己实现这个程序吧! :)
    其实我更想把注释也读取出来(包伙简单和详细的2个版本),这样的话,岂不是俺可以卖一个XX词霸简化版本?相信大家使用词霸的时候,对于发音、生词什么的功能使用得不多吧?
      

  4.   

    楼主强。
    可以把你做的MDB文件传到网上,用ASP根据单词查询MDB数据库,这样,一个在线词霸网站就成了。呵呵。
      

  5.   

    程序又做了改进,可以读取金山词霸的单词解释啦!
    请到我的blog:
    http://blog.csdn.net/ecai/archive/2004/06/26/27453.aspx
      

  6.   

    既然这么多朋友要求源代码,我还是发表出来吧,包括读取单词列表和单词解释2部分
    http://blog.csdn.net/ecai/archive/2004/06/27/27696.aspx
      

  7.   

    查找窗口似乎用FindWindow和FindWindowEx更为方便吧
      

  8.   

    呵呵.我做到网上吧.
    用mysql做数据库.
      

  9.   

    谁能把词库传到我的FTP:
    ftp://218.21.71.201
    上传帐号和密码都是:upload
    下载帐号和密码:gbpc
    做好后我做成网页查询方式放到互联网上.