使用钩子记录鼠标点击处网页元素

来源:互联网 发布:毕向东java教学视频 编辑:程序博客网 时间:2024/06/17 17:35

1、目的

  研究自动化测试中的录制、回放实现方法。通过钩子录制鼠标、键盘的记录,然后再进行回放。

2、方法

  windows下的钩子有很多类型,这里采用WH_JOURNALRECORD和WH_JOURNALPLAYBACK这一对钩子实现录制和回放。

2.1 钩子的安装

  在所有钩子中,WH_JOURNALRECORD和WH_JOURNALPLAYBACK是特殊的一对,可以不通过DLL就能够实现全局效果。因此只需要在主程序中通过调用windows api 安装即可。

  SetWindowsHookEx(WH_JOURNALRECORD,JournalRecordProc,hinstance,0);

2.2 钩子处理函数

  根据WH_JOURNALRECORD钩子的标准定义,钩子处理函数定义为:

  LRESULT CALLBACK JournalRecordProc(int code,WPARAM wparam,LPARAM lparam)

  其中参数lparam为传进的消息。

  如果只需要将消息记录,那么通过下面的代码就可以实现:

  switch (code)

{

 case HC_ACTION: 

           mesg=(EVENTMSG *)lparam;

           if (mesg->hwnd!=thiswindow_hwnd){

           mesg->time=mesg->time-start_time;

 

           output->write((const char *)mesg,sizeof(EVENTMSG));  //output为输出文件流

           output->flush();

 

       }

 

          break;

 default:

          return CallNextHookEx(hook_handle,code,wparam,lparam);

 

  }

但是如果只记录消息,不利于录制之后的二次编辑,不利于实现关键字驱动的自动化测试。因此,需要在记录时对消息进行提取,简单的讲就是当鼠标点击网页时,钩子只能记录一个鼠标点击消息。但是我们还需要得到点击的网页元素是什么,id是什么之类的信息。

根据鼠标点击时的坐标,我们可以得到HTML信息。

2.3 根据坐标获得网页元素

  我们可以根据IHTMLDocument2接口提供的方法获得所在坐标的html element。

   HRESULT elementFromPoint(long x,

                                                long y, 

                                                  IHTMLElement **elementHit );

  现在的问题是我们只有通过钩子记录的鼠标点击发送的消息。没关系,我们对消息进行分析。当钩子记录的消息类型为WM_LBUTTONDOWN时,消息的参数paramL表示横坐标,paramH表示纵坐标。这下坐标有了,就差获得当前网页窗口的IHTMLDocument2对象了。

2.4 根据句柄获得IHTMLDocument2对象

   我们现在能够得到当前浏览器窗口的句柄,因为当鼠标点击网页元素时,WM_LBUTTONDOWN消息的hwnd参数记录的正是该消息发送的目标窗口句柄,即当前操作的浏览器窗口句柄。通过调用"OLEACC.DLL"库来实现。以下代码来自MSDN的开源代码,可以直接使用。

 

IHTMLDocument2* GetDocInterface(HWND hWnd) 

{

  CoInitialize( NULL );

 // 我们需要显示地装载OLEACC.DLL,这样我们才知道有没有安装MSAA

 HINSTANCE hInst = ::LoadLibrary( _T("OLEACC.DLL") );

 IHTMLDocument2* pDoc2=NULL;

 if ( hInst != NULL ){

  if ( hWnd != NULL ){

   CComPtr< IHTMLDocument2 > spDoc=NULL;

   LRESULT lRes;

  // 由于WM_HTML_GETOBJECT非Windows标准消息,所以需要RegisterWindowMessage

   UINT nMsg = ::RegisterWindowMessage( _T("WM_HTML_GETOBJECT") );

   ::SendMessageTimeout( hWnd, 

       nMsg, 

       0L, 

       0L, 

       SMTO_ABORTIFHUNG, 

       1000, 

       (DWORD*)&lRes );

   //取得ObjectFromLresult函数的地址

   LPFNOBJECTFROMLRESULT pfObjectFromLresult =(LPFNOBJECTFROMLRESULT)::GetProcAddress( hInst, _T("ObjectFromLresult") );

   if ( pfObjectFromLresult != NULL ){

  HRESULT hr;

hr = (*pfObjectFromLresult)(lRes,IID_IHTMLDocument, 0,(void**)&spDoc);

  if ( SUCCEEDED(hr) ){

     CComPtr< IDispatch > spDisp;

     CComQIPtr< IHTMLWindow2 > spWin;

     spDoc->get_Script( &spDisp );

     spWin = spDisp;

     spWin->get_document( &pDoc2 );

    }

   }

  }

  ::FreeLibrary(hInst);

 } 

 else{//如果没有安装MSAA

  AfxMessageBox(_T("请您安装Microsoft Active Accessibility"));

 }

 return pDoc2;

}

2.5 困扰很长时间的问题

  一切看起来都是那么完美,好像问题迎刃而解?

  但是运行时发现,钩子一开始记录网页,就卡死。但是单步调试没有问题。仔细分析,发现了问题的所在。

  当钩子被启动以后,原来程序是在钩子中调用GetDocInterface方法,目的是获得坐标处的HTML,但是仔细看看GetDocInterface方法。其中,SendMessageTimeout函数的含义是,如果是在跨线程中调用,则必须等到接收消息的窗口处理完原信息之后再响应该消息。此时,发送消息的是钩子线程,而接收消息的是浏览器。那么意味着钩子线程必须等待直至浏览器处理完消息,但是浏览器此时还需等待钩子程序对钩子消息进行处理。这样就形成了死锁。

   解决的办法是,在钩子处理函数中,创建一个线程,该线程用来处理GetDocInterface函数原本处理的工作。这样钩子处理程序就不需要等待SendMessageTimeout消息的处理,这样就不会形成死锁。

   这样处理之后,实验证明问题解决。

2.6其它小问题

  包括用户区坐标和屏幕坐标的转换等小问题,也要注意。