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

来源:互联网 发布:淘宝分享链接微信屏蔽 编辑:程序博客网 时间:2024/06/08 03:31

让你的VC也支持手势----【手势扒手】制作过程 下 收藏 此文于2010-12-01被推荐到CSDN首页
如何被推荐?
 让你的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_COMMND

lpCommndData 在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