让你的VC也支持手势----【手势扒手】制作过程 下 - gangyilovevc的专栏 ...

来源:百度文库 编辑:神马文学网 时间:2024/04/20 22:43:17
 让你的VC也支持手势----【手势扒手】制作过程 下 接上篇,好,我们继续,在剩下的文字中,(还是有点长,就不在分了)我们重点剖析下插件主要部分的设计思想和实现,对其做个简单总结。前面说过,插件采用VC的add-in实现,他是一个基于ATL/COM的组件应用程序,如果不了解ATL/com的也无所谓(了解当然更好啦。。),因为涉及到他们东西很少,它最重要的地方就是提供了一个框架壳子,而且此框架也不必深究,都交给IDE来处理,我们更多还是用MFC,这个应该熟悉把,最起码也应该会用把,如果不会。。。。。,厄,那也能看懂,因为我们重点说思想,不涉及具体的代码,当然示例性代码还是有的,嘿嘿。新建一工程,命名为MouseGesture,向导选择DevStudio Add-in Wizard,单击OK,来到add-in向导第一步,我们插件也提供一个工具栏,可以保持默认,单击完成,这时具有扩展IDE开发环境的插件已经建好了,编译成功后,通过 Tools—Customize---Andin and Macro Files标签页将我们的插件添加道IDE中,成功后应该在IDE中显示一个新的工具栏,后续工作我们都以此工程为基础实现我们自己的插件。现在就让我们一步步把程序核心关键地方一一讲解,当你看完时,也许你会这么想,“就这么简单啊,我也来做一个。。。”开始啦~~~~~~!1.  既然是手势,鼠标相关的信息我们得知道把,这里我们通过鼠标钩子来实现,分别在插件初始化和卸载时实现钩子的挂钩及脱钩。代码简单的连注释都省了。如下。 //挂钩CDSAddIn::OnConnection(){          ………….   g_hook = SetWindowsHookEx(WH_MOUSE,&MouseProc,NULL,GetCurrentThreadId());}//脱钩CDSAddIn::OnDisconnection(){      …………………..      UnhookWindowsHookEx(g_hook);}2.  鼠标的消息我们已经可以拿到了,先让我们把鼠标消息转换成屏幕上的线把,只要完成这一步手势看着就有点样子。程序中我们实现了一个类CMouseEx,由他来实现所有功能,先看下他的声明,后面用的到。class CMouseEx  {      ………………..//程序本身提供的功能      void CloseCur(); //关闭当前窗口,      //加速键相关      //WM_COMMND相关      ……………………}关于用鼠标划线的功能,想必大家都不陌生,无非是记住鼠标当前位置(我们已经下钩了),然后MoveTo、LinetTo之类的,这些就不在说了,这里着重要说的是在那个DC上作画?DC有没有裁剪?由于我们程序是一个辅助的划线工具,不可能让线画到整个IDE环境,比方说菜单、工具栏、类视图等这些地方是不允许划线的,要不太乱了,而且平时我们大多也在代码编辑地方待的时间最长,所以这里应该是理想手势执行区。所以呢, DC我们采用屏幕DC,屏幕DC对我们来说已经够用了,操作范围以代码编辑区为界限。决定了使用屏幕DC,和操作DC范围,看看如何得到他把,得到屏幕DC应该很简单的,如下。if (!m_pWindowDC){m_pWindowDC = new CWindowDC(CWnd::FromHandle(GetDesktopWindow()));}  以后操作都针对此DC,由于此DC可以操作到整个屏幕,刚才我们也说过应该将范围限制在代码编辑区中,所以呢给此DC加个裁剪,这个怎么做呢?代码编辑区其实也是一个窗口,具体窗口的属性大家可以通过spy++看看,由此知道他其实是一个类名为MDIClient的窗口,已打开的文件是他的子窗口。那现在就很明显了,得到这个MDIClient窗口句柄,在得到他的RECT,然后在以此RECT对DC做裁剪,这样就把操作限制在代码编辑区了。  MDIClient窗口同时有是IDE的一个子窗口,显然我们要先得到IDE的窗口句柄,然后遍历其子窗口,在查找类名为MDIClient的窗口即可。好,继续看代码。//寻找IDE 句柄 原理就是IDE的父窗口为NULL      HWND hWnd, hDesktopWnd;      hWnd = ::GetActiveWindow();      hDesktopWnd = ::GetDesktopWindow();      while (hWnd  &&  hWnd != hDesktopWnd)    {        m_hDevStudioWnd = hWnd;        hWnd = ::GetParent(hWnd);    }     CWnd *pDevStudioWnd = CWnd::FromHandle(m_hDevStudioWnd);       //找到IDE句柄了在根据类名查找MDIClient子窗口      char szClassName[256];            m_hMDIWnd = pDevStudioWnd->GetTopWindow()->m_hWnd;   ::GetClassName(m_hMDIWnd, (LPTSTR)szClassName, sizeof(szClassName));      while (strcmp(szClassName, "MDIClient") != 0)      {               m_hMDIWnd = ::GetNextWindow(m_hMDIWnd, GW_HWNDNEXT);               ::GetClassName(m_hMDIWnd, (LPTSTR)szClassName, sizeof(szClassName));        }    //给DC加裁剪CRgn rgn;rgn.CreateRectRgnIndirect(&rcClient);            m_pWindowDC->SelectClipRgn(&rgn);到这里时,DC和裁剪都有了,在加上划线功能,恩,有点手势味道了,我们划线采用Polyline方式,因为我们手势还有tip显示功能,如果用Move\Line画线的话,Tip窗口可能将我们划的线给擦掉,简单期间我们直接使用Polyline方式,让他每次都重新画一边。3.  鼠标消息、划线功能都有了,现在应该说手势命令识别了,怎样识别手势呢?起初我也想复杂了,当时是想到了文字的识别?那肯定是很复杂的,但其实那些我们用不到,我们用到的只是记录鼠标移动过的轨迹,并辨别它,比如说RD、LD之类的,(U -上,D-下,L-左,R-右)核心代码也是从网上摘录的,代码很简单也很经典,直接看代码吧。//生成手势命令BOOL CMouseEx::MakeMouseGestureCommnd( CPoint pt ){     int x = pt.x - m_ptGenerateCur.x;         int y = pt.y - m_ptGenerateCur.y;         int dist = x*x+y*y;          if(dist>64)                        {                      if(x>abs(y) && x>0)              {// R              }              else if(abs(x)>abs(y) && x<0)              {// L              }              else if(y>abs(x) && y>0)              {//D                }              else if(abs(y)>abs(x) && y<0)              {// U              }              else return FALSE;                  return TRUE;                  }             else          return FALSE;   }         没骗你把,很简单不是。4.  现在应该到了具体的功能了,程序支持三种手势功能分别是本身实现的、通过快捷键的、发送WM_COMMND的,下面一一介绍。a)  程序本身的功能在这个模块可以实现一些自己特有功能(总不能老拿别人的把),或者对某个功能扩充,好处吗?自然很明显,举个简单例子,对话框应用程序中,往往是在OnInitDialog中对一些控件变量初始化,然后在OnOK中把其值的在拿回来,类似如下这样。CInfo m_info;//举例 一个类,属性对应dlg每个控件OnInitDialog(){//edit 控件     m_EditName = m_info.m_csName;     m_EditAge = m_info.m_csAge;     m_EditSex = m_info.m_csSex;  …… //假设有很多edit控件需要赋值 }OnOK(){//点击确定后,要拿到最新值,怎么做呢?反正我是这样做的,上面代码拷贝过来,一个一个在修改,也就是修改下等号位置,但往往这些赋值操作很多,让我改的很痛苦。。。。m_info.m_csName = m_EditName;m_info.m_csAge= m_ EditAge;m_info.m_ csSex = m_EditSex ;………//很多}如果我将这个手工操作改为用代码来实现,并且通过手势来触发,以后在遇到此类问题,我首先选中一段代码,然后鼠标一画便实现反向赋值,很爽不是吗?恩,这个是挺好,原有的一个一个改确实很烦,但。。。这个功能我却没有实现,到不是复杂,关键是类似这样问题太多了。。。。现在插件本身的功能,没有一个是自己实现的,都只使用了接口IApplication本身的几个方法,实现了几个功能?主要还是‘偷‘现有功能。呵呵。b)  快捷键之前我的想法是,可以把所有的快捷键都转换成手势,都可以用手势来触发,但那毕竟只是一个想法而已,在真正实现的时候,发现了一个让人极度不爽的事情,那就是对快捷键有个特殊要求,必须带ALT 否则不起作用,触发快捷键我采用keybd_event/ SendInput方式,但发现如果不带ALT的话,就相应应不了,原因还不太确定?所以不便多说,如果有知道的朋友,请告诉我。不过那并不代表我们就没有办法了,可以把我们频繁用到的功能(主要针对IDE),重新给他指定一个带ALT的快捷键,在 Tools—Customize---Keyboard中设置,然后将此快捷键添加到配置中,我么可以一样用。呵呵,条条大路通罗马。c)  WM_COMMND如果不愿意重新设置快捷键、或者某个功能在Keyboard没有、在或者需要‘偷‘某插件功能以及其他?那怎么办?这是就用到了发送消息方法了, 理论上所有基于发送WM_COMMND实现的功能我们都能够‘偷’过来,因为本身就是发送了一个WM_COMMND消息而已。WM_COMMND详细介绍请参见MSDN,这里简单说明一下我们的使用原理,基于菜单、加速键的功能都是通过发送WM_COMMND来实现的,HIWORD(wParam) 1 来自己菜单 0 来自加速键LOWORD(wParam) 控件标示ID。其实这个功能实现起来是最简单的,就是发送一个WM_COMMND消息,但他功能也是最强的,等等。。。。。控件标示ID怎么得到,单击一个菜单项,或者一个加速键我怎么知道他的标示ID,你如果能想到这点,证明你是一个心思缜密的人,没想到的证明你是一个做大事的人。嘿嘿。。。。不错,我们还没有得到这个ID?没有这个ID我们的消息也没有办法发啊?怎么拿呢?应该有更好的办法把,反正我没用,也没具体想,我只是拿出了SPY++,监视IDE消息,因为没有用的消息太多,为了不影响输出,我只过滤了WM_COMMND消息,然后在IDE中操作一下我将要‘偷’的功能,完毕后,我在看看SPY++输出什么,然后将对应的信息,输入到程序配置中 ,仅次而已。恩,是的,虽然麻烦但他的确可以工作,而且平时我也不会在修改它,真可谓一劳永逸。。。。。。5.  具体功能总是对应具体实现,如果明白了上述功能的原理,那么看下面的结构应该是很简单的。让我们看看上述功能如何用程序来描述。 //0 软件功能 1 扒 快捷键 2 扒 菜单命令typedef enum {Commnd_Self=0x00,Commnd_ShorCut,Commnd_Menu} CommndType;typedef struct {           CommndType  type;           char*                 lpCommndData; //数据           CString        csMouseGesture; //鼠标手势 LRUD           CString    csDesc; //描述 用于tip}MouseCommnd;不错,一个结构而已,相当于上述功能快照。也许唯一点要说明的就是lpCommndData了。不同手势类型代表不同数据(也许用个union比较好,做的时候,是想到那就做到那,有的地方也许欠考虑。。。),在最后执行命令时,根据type类型,做不同处理,如下。BOOL CMouseEx::Excute(){      MouseCommnd* pCommnd;switch (pCommnd->type){          case Commnd_Self:                  //程序本身的功能case Commnd_ShorCut             //处理加速键case Commnd_Menu:               //发送WM_COMMND}}下面对每个不同类型的lpCommndData数据格式做个简要分析。a.        类型为Commnd_Self的,代表程序本身实现的功能,说白了就是实现一函数来完成某个具体逻辑, 因为提供功能已经事先定好,所以我们在lpCommndData身上存放函数地址,这样在执行时,我们可以在将其转化为函数调用,之后这块逻辑就不用在修改了,以后在增加功能,只要存放正确函数地址即可,为此用四个字节,存放函数地址。怎样确定一个函数地址呢?而且还是类成员函数,比方说就前面提到过的CloseCur这个成员方法,这个成员地址怎么得到呢?其实地址在编译时就已经确定,可以通过类的偏移来得到地址,但那样如果类结构改了,就有可能出错,为此这里我们根据堆栈平衡原则来得到函数地址。实现一函数,参数为函数指针,将成员函数传过去,这时堆栈里面就存放该函数地址了,我们得到他就可以了。描述有可能不清,直接来代码把,正所谓‘源码之下,了无秘密’,相信大家能看懂的。。。//得先有一个成员函数指针把typedef void (CMouseEx::*pSelffn)(); //如下是填充一个程序本身功能的代码。使用时像这样MouseCommnd *pCommnd = new MouseCommnd;pCommnd->type = Commnd_Self;pCommnd->lpCommndData = new char[4];//得到函数地址DWORD dAddress  =  GetFunAddress(&CMouseEx::CloseCur); *(DWORD*)pCommnd->lpCommndData = dAddress;pCommnd->csMouseGesture = "DR";pCommnd->csDesc = "关闭当前窗口";//、、、、、、可以增加更多功能 //采用内联汇编方式,得到成员函数的地址DWORD CMouseEx::GetFunAddress( pSelffn p ){DWORD dAddress;         __asm         {                   mov eax,[ebp+08h]                   mov DWORD ptr[dAddress],eax         }         return dAddress;}在函数调用时,无非也就是先PUSH 参数、push返回地址,、push ebp ,其中[EBP+8]为最右边参数,这里就一个参数,也就是说这个值,既函数地址。(以VC6以及stdcall 调用约定来解析)这样我们可以得到所有函数地址,在调用时就方便了。如下接上面BOOL CMouseEx::Excute() 函数case Commnd_Self: { //我们只保存函数地址,所以得自己实现函数调用,模拟CALL指令       DWORD *dwThis = (DWORD*)this;                                     __asm       {                 mov ebx,dword ptr [pCommnd]                 mov edx,[ebx].lpCommndData                 mov eax,dword ptr [edx] ;得到函数地址                 mov ecx, dword ptr [dwThis]   ;模拟this指针                 call  eax          ;调用函数,无参        }}应该挺简单的把。。。。VC中如果想看反汇编代码,ALT+8能够满足你,那多麻烦,用手势多好。这样做的好处时,不管你以后在增加多少功能,只要初始化函数地址正确,它就能够正常工作,也就是说你要的工作是初始化pCommnd这个结构,并实现函数的具体逻辑,仅此而已,其他的你用管。这一块是不需要修改的。当然除了BUG。b.   加速键lpCommndData 在加速键类型中,存放hotkey的值,大小也为四个字节,高位放wModifiers,低位放wVirtualKeyCode。上面已经说过,快捷键要起效,就必须带ALT,其实触发快捷键那就简单了keybd_event 完全可以胜任,具体实现就不用在举例了把。无非也就如下这样keybd_event(wVirtualKeyCode,0,0,0); //模拟键按下keybd_event(wVirtualKeyCode,0,KEYEVENTF_KEYUP,0);//弹起c.    WM_COMMNDlpCommndData 在COMMND中,用于存放消息,大小也为四个字节,高位放加速键、菜单项的标示ID,低位放Notification Cod,既加速键为1,菜单为0。至于发消息,那就更简单了,只要信息正确,如下OK。USHORT nCode,nFlag;nCode = *( USHORT *)pCommnd->lpCommndData;nFlag = *( USHORT *)(pCommnd->lpCommndData + 2);// 偏移消息号//向IDE 发消息。PostMessage(m_hDevStudioWnd,WM_COMMAND,MAKEWPARAM(nCode,nFlag),0);6.  至此对程序核心部分的设计思想及实现我做了一些简要分析,我希望我表达的够清楚,以不至使你既浪费了时间,有没有搞清楚怎么回事?如果真的那样,请不要怀疑你的能力,那是我的问题,怪我没有事情讲清楚。我们分析了程序主要部分,当然这个小程序还涉及到一些其他知识点,麻雀虽小,但也五脏俱全,比方说Ø   为了限制输入手势命令的EDIT只能接受几个特定的字母,我使用了子类化Ø   移动tip窗口,并对其贴图。Ø   增加手势命令的BUTTON ,我使用了重绘了。Ø   以及其他小地方。但这样的一些东西,一不是我们分析目标,二也没有必要说这些东西,毕竟我们主要着重点是在手势扒手。      看下我的测试环境。 
最后在说说源代码的事,代码在XP_SP2、VC_SP6下编译并通过简单测试,其实我也想过把代码放出来,但由于最初没有定具体功能,在做的时候,是想道那就做到那,感觉好的就加上了,代码太。。。,实在不好意思拿出来误导大家,不过核心思想我们已经讲过了,你也完全可以重新实现一个,随便一写也比这个好。      好了,终于完了,就到这里把,祝大家愉快~~~~~~
插件下载地址:http://download.csdn.net/source/2867221 本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/gangyilovevc/archive/2010/11/29/6042014.aspx