代码改变世界

[开源] C语言项目实战 - 虚拟鼠标 - VirtualMouse

2013-03-26 06:13  wid  阅读(13448)  评论(14编辑  收藏  举报

C语言项目实战 - 虚拟鼠标 - VirtualMouse

目录


 




软件介绍

VirtualMouse是一款简单的通过键盘模拟鼠标行为的软件, 使用 BSD 开源协议, 软件采用 C语言 + Windows API 进行编写, 程序仅 20KB, 对资源消耗极低, 在一定程度上能够代替实体鼠标对 Windows 进行相关的操作。

 

软件截图:

 

 




使用帮助

VirtualMouse采用组合键进行操作, 当需要使用鼠标时, 按住键盘上的 Ctrl 键(不放开, 左/右Ctrl皆可)激活该软件, 具体的组合方案如图所示:



例如您要使用键盘代替鼠标进行指针的向上移动并完成一次左键单击, 那么您需要先按住Ctrl键, 不能松开, 再按下小键盘盘中的数字键 8(这里默认每次移动40像素), 若移动幅度过大, 可再按下方向键的上下左右进行相关的调节, 将指针移动到指定位置后, 按下小键盘中的数字键5, 完成鼠标的一次左击。

程序按钮以及其他热键说明:

按钮: 暂停模拟 暂停程序对模拟的支持, 当按下该按钮后, 该按钮的文字会变为 继续模拟, 点击后程序继续对鼠标进行模拟。

按钮: 隐藏窗口 将程序完全隐藏转入到后台运行。

按钮: 退出模拟 停止模拟并退出程序。

 

除以上图中对鼠标操作模拟的组合键外, 程序还支持使用热键对模拟的暂停与继续:

Ctrl + F10: 暂停模拟, 再按一次继续模拟。该热键同时会关联到主界面的按钮文字, 若您不慎忘记此时是否已经开启模拟, 可 Ctrl + F12: 查看按钮状态进行辨别。

Ctrl + F12: 用于当程序窗口完全隐藏时呼出程序主界面。




项目介绍




源码及软件下载

 

软件下载: https://files.cnblogs.com/mr-wid/VirtualMouse_release.zip

 

源码下载: https://files.cnblogs.com/mr-wid/VirtualMouse_project.zip

 

注: 提供的源码下载中已包含 VC6 的工程文件, 使用VC6直接打开项目即可, 更高版本的VS进行工程转换即可。


 

 

 

 

知识点扫描

 

 

1. 热键的使用

全局热键属于系统资源, 在在程序中使用热键比较简单, 主要分为两个步骤: 一是向系统注册热键键值, 二是处理热键消息。注册热键通过 RegisterHotKey 函数进行, 该函数的原型:

        BOOL RegisterHotKey(
          __in  HWND hWnd,                //用来接收 WM_HOTKEY 热键消息的窗口句柄
          __in  int id,                    //热键的ID, 当注册多个热键时用ID区分
          __in  UINT fsModifiers,            //热键的主键, 可为 MOD_CONTRO(Ctrl键)、MOD_ALT (Alt键)、MOD_SHIFT (Shift键)等等或其中的组合
          __in  UINT vk                    //热键的副键, 以 VK_ 开头的标识符
        );

例如我们要用 Ctrl + F9 作为某程序的热键, 向系统注册的函数调用如下:

RegisterHotKey( hwnd, 10000, MOD_CONTROL, VK_F9 ); //注册ID为10000, 组合为 Ctrl + F9 的热键

向系统注册热键后, 便可在 hwnd 所在的回调函数中来处理热键消息, 当热键被按下时, 系统发来的消息类型为 WM_HOTKEY , 消息的 wParam 参数为热键的ID, lParam高位的字节为热键的主键类型, 低位字节为热键副键类型。例如在 hwnd 的回调函数中处理:

        case WM_HOTKEY:
            switch( wParam )
            {
                case 10000:
                    MessageBox( NULL, TEXT("热键 Ctrl + F9 被按下"), TEXT("热键消息"), MB_OK );
                    breal;
            }
            return 0;

当热键不使用时应该向系统撤销热键, 撤销热键的函数为:

            UnregisterHotKey(
                HWND hwnd,        //注册热键时所用的窗口句柄
                int id            //热键ID
            );

关于热键ID的说明: 根据MSDN上介绍, 当程序与其他程序共享一个动态链接库时, 为了避免热键ID的冲突, 应使用 GlobalAddAtom 函数来向系统获取一个唯一的ID, 虽然说这种ID冲突的概率是比较低的, 但是为了程序的健壮性, 最好使用 GlobalAddAtom 向获取这个唯一的ID。

 

2. 鼠标事件的模拟

鼠标事件的模拟这里采用的是使用 mouse_event 函数, mouse_event 函数可模拟一般鼠标的所有操作, 该函数的原型为:

        VOID mouse_event(
          __in  DWORD dwFlags,           //鼠标状态标识符
          __in  DWORD dx,              //指针x坐标
          __in  DWORD dy,              //指针y坐标
          __in  DWORD dwData,           //当参数一为MOUSEEVENTF_WHEEL时, 该值表示滑轮的移动数量, 向上为正, 向下为负
          __in  ULONG_PTR dwExtraInfo   //消息附加值
        );
		参数中的鼠标状态标识符如下:
            MOUSEEVENTF_MOVE                 //鼠标发送移动
            MOUSEEVENTF_LEFTDOWN             //鼠标左键按下
            MOUSEEVENTF_LEFTUP                //鼠标左键抬起
            MOUSEEVENTF_RIGHTDOWN             //鼠标右键按下
            MOUSEEVENTF_RIGHTUP                //鼠标右键抬起
            MOUSEEVENTF_MIDDLEDOWN             //鼠标中键按下
            MOUSEEVENTF_MIDDLEUP               //鼠标中键抬起
            MOUSEEVENTF_WHEEL                  //滑轮被滚动
            MOUSEEVENTF_ABSOLUTE               //是否采用绝对坐标

例如模拟一次鼠标在当前指针位置的左键单击效果:

            mouse_event( MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0 );        //左键被按下
            mouse_event( MOUSEEVENTF_LEFTUP,   0, 0, 0, 0 );        //左键被抬起

在实际应用方面, 该函数若配合 GetWindowRect 以及 Spy++ 可方便的完成一些自动点击工作。

 

3. 字体的选用

在C语言Windows编程时, 一些标准控件的字体是十分难看的, 可以说是又黑又粗, 还挤在了一起, 这十分影响用户在UI上的体验, 因此我们要自己选用一些对用户UI体验友好的字体来替换掉标准控件的字体。要使用从系统中选择出的逻辑字体, 一般来说分为三个步骤:

1. 填充 LOGFONT 中的字段;

2. 根据 LOGFONT 中的字段创建出逻辑字体;

3. 将创建的逻辑字体选入到环境设备;

关于 LOGFONT 这个结构, 它的字段比较多, 有 14 个, 该结构的原型如下:


		typedef struct tagLOGFONT {
		  LONG  lfHeight;				//期望的字体高度
		  LONG  lfWidth;				//期望的字体宽度
		  LONG  lfEscapement;			    //字符串的方向
		  LONG  lfOrientation;			    //单个字符的倾斜方向
		  LONG  lfWeight;				//字体的粗细
		  BYTE  lfItalic;				//是否使用斜体
		  BYTE  lfUnderline;			    //是否加下划线
		  BYTE  lfStrikeOut;			    //是否加删除线
		  BYTE  lfCharSet;				//设置文字的字符集
		  BYTE  lfOutPrecision;			    //字符输出的精度
		  BYTE  lfClipPrecision;		    //超出显示区域的裁剪方式
		  BYTE  lfQuality;				//定义字体的输出质量
		  BYTE  lfPitchAndFamily;		    //指定字体的字符间距和族
		  TCHAR lfFaceName[LF_FACESIZE];        //所要使用的字体名称
		} LOGFONT;
			

对于这个结构更加详细的信息, 限于篇幅原因这里不再一一描述, 继续说对于逻辑字体的创建, 当完成 LOGFONT 结构的填充后, 调用 CreateFontIndirect 函数得到程序返回的指向一个逻辑字体的句柄, 该函数仅有一个参数, 也就是 一个指向 LOGFONT 结构的指针, 其函数原型如下:

HFONT CreateFontIndirect( const LOGFONT *lplf );

调用该函数, 得到逻辑字体的句柄后便可以将该字体应用到任何一个需要使用字体的环境设备中了, 以在窗口上绘制字体为例:

        case WM_PAINT:                            //处理重绘消息
            hdc = BeginPaint (hwnd, &ps) ;        //获得设备环境句柄
            hFont = CreateFontIndirect(&lf);      //调用 CreateFontIndirect 函数获取逻辑字体句柄, lf 即为 LOGFONT 结构创建的变量
            SelectObject (hdc, hFont ) ;          //将逻辑字体 hFont 选入环境设备
            TextOut ( hdc, 10, 10, TEXT("Hello, 世界!"), lstrlen (TEXT("Hello, 世界!")) ) ;        //使用 TextOut 函数在窗口上输出文字
            DeleteObject( hFont );                //字体使用完毕后删逻辑字体
            EndPaint (hwnd, &ps) ;
            return 0;

 

当字体不再使用时, 应删除该字体的逻辑字体, 即便每创建一个逻辑字体所占用的资源并不多, 甚至可以忽略, 但是像在 WM_PAINT 这样的消息中创建字体就必须得重视了, 重绘消息可是无穷无尽的, 会造成内存的泄露。

能够输出字体还不算真正解决上述的问题, 我们还得改变标准控件中的字体才行, 要改变控件的字体, 一个十分方便的方式就是使用 SendMessage 函数向控件发送改变字体的消息, 一个改变按钮控件上的字体示例:

      SendMessage( hwndBtn, WM_SETFONT, (WPARAM)hFont, 0 );

 

hwndBtn 为待改变字体的控件句柄, WM_SETFONT 是通知 hwndBtn 改变字体的消息, WPARAM 参数为刚刚通过 CreateFontIndirect 函数创建的逻辑字体的句柄。

 

4. 开机自启动的设置

在 Windows 下, 要让一个程序开机自动运行, 只要将该程序的启动路径添加到注册表的开机自启动项即可, 该项在注册表的位置为:

HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run

在 "开始" -> "运行" 中输入 "regidit" 确定后进入到注册表编辑器, 找到该项所在的位置后可以看到, 像杀毒软件以及显卡、声卡管理等程序的路径已经被添加到开机这里了, 我们现在要做的, 就是在这里写入本程序所在的路径, 让他在下次开机时也能够开机自启动。

通过注册表管理API可以实现对注册表的自动操作, 关于对注册表进行管理的全部函数, 可以查阅 MSDN Library 的 "Registry Reference" 主题, 此处仅讨论下如何将该程序设为开机自启动。

这里需要用到三个函数, RegOpenKeyExRegSetValueExRegCloseKey

RegOpenKeyEx : 用于打开一个指定的注册表项, 类似于文件的操作, 要写入东西, 得先打开;

RegSetValueEx: 用于向指定项中写入新值;

RegCloseKey: 操作完成后将注册表关闭。

通过代码将自身路径加入到该项的程序如下:

        //设置开机自启动
        int setVMAutorun()
        {
            HKEY hKey;        //注册表项的句柄
            long lRet = RegOpenKeyEx( HKEY_LOCAL_MACHINE, "Software\\Microsoft\\Windows\\CurrentVersion\\Run", 0, KEY_WRITE, &hKey );        //打开指定项到 hKey
            if(lRet == ERROR_SUCCESS)         //打开成功
            {
                wchar_t pFilePath[MAX_PATH] = {0};            //存放当前程序路径
                DWORD dwRet = GetModuleFileName(NULL, (char*)pFilePath, MAX_PATH);        //获取当前程序运行路径
                lRet = RegSetValueEx( hKey, "VirtualMouse", 0, REG_SZ, (BYTE *)pFilePath, dwRet*2 );    //新建值"VirtualMouse", 数据为获得的程序路径
                RegCloseKey(hKey);        //操作完毕, 关闭
            }
            if( lRet == ERROR_SUCCESS )
                return 1;
            return 0;
        }

 

在该项目中, 笔者还实现了检查是否已创建开机自启动和取消开机自启动的功能, 实现过程在 set_autorun.c 文件中。

 

5. 为软件添加版本信息

    如图所示, 为了让该程序能够更加像一个完善的软件, 笔者还像其他大程序一样给该程序添加了一些其他信息, 让用户能够在不打开软件的情况下就能看到关于该程序的相关描述。通过 Visual C++ 6 对程序添加说明信息的步骤如下:

      1. 新建一个资源文件, 菜单 -> 新建 -> 文件 -> 选择资源脚本 (如果你还没有新建的话);

      2. 在工程管理的资源选项卡中, 对资源进行 "插入" -> 资源类型 Version -> 选择新建; 然后在出现的 Version 类型资源的编辑窗口中完成对程序的描述, 再让其参与项目的编译即可。

 




想不到的事

1. 不翼而飞的按钮

在一个偶然的情况下, 笔者将杀毒软件(金山)关闭后, 在运行 VirtualMouse 时惊奇的发现, 这个软件主界面上的4个控件, 3个按钮和一个复选框居然不见了! 起初笔者认为是程序的代码问题, 但是经过一番检查后并没有发现代码的问题所在, 于是将代码发给了我的一位朋友一起进行检查, 还是没有结果。 若干分钟后, 笔者又把杀毒软件启动了, 继续找按钮消失的原因, 这时, 见证奇迹的时刻到了! 按钮居然又自己回来了!

经过反复的测试确认后, 笔者把问题的原因锁定在金山毒霸这个软件上面, 只要金山毒霸处于启动状态, 按钮就存在, 将毒霸完全退出后, 再运行本程序, 按钮立即就没了。

笔者这时这是纠结啊! 各种纠结, 最痛苦的就是这种情况了, 各种 google, 各种 search, 都没有找到有出现类似情况的案例给予参考, 只能完全摸索着解决了。

幸运的是, 最终的原因还是找到了, 下面是排查过程:

1. 大段的注释代码, 使不影响界面运行的代码全部注释掉, 再编译运行测试; 结果: 未能解决这个问题, 确认了该问题与代码无关。

2. 新建一个同类型的win32工程, 仅创建一个窗口和一个按钮, 所有的消息全部交给默认处理函数进行; 结果: 按钮依然存在, 没有出现问题。

3. 对比源工程与新工程, 找不同, 发现一个十分不同的情况就是源工程与新工程的按钮外观不同, 也就是风格不同, 因为笔者在 VirtualMouse 工程里使用了 manifest 文件, 指定控件的风格是使用新式控件; 结果: 猜测消失的原因可能和 manifest 资源文件的添加有关。

4. 删除 VirtualMouse 工程下的manifest资源文件, 使用默认的旧的样式, 编译运行测试, 按钮没有消失, 但是风格为旧式风格, 十分难看; 结果: 确认了按钮消失的原因和manifest文件有关。

5. 笔者将得到的结果告诉刚才和我一起找代码问题的朋友, 他回复到, 猜测到原因的所在了。

我们推测的结果:

我们知道, 在系统中, 存在不同版本的控件, 这些新旧版本在不同的dll中, 控件的使用需要先初始化这些dll, 金山在启动时会默认初始化新式控件, 以便给自己的程序使用, 笔者的程序没有调用这些dll的初始化工作, 而是由毒霸代替了, 毒霸在退出时, 又对新式控件的dll进行了析构之类的操作, 使其恢复到未初始化的状态, 因此笔者的程序中又是指定是用新式控件, 然而新式控件的dll此时又未被初始化, 所以无法完成新式按钮的创建, 故导致按钮的消失。

解决方案:

找到了问题的原因, 问题自然就好解决了, 原因就是新式控件样式未被初始化, 那么我们就添加相关的初始化工作好了, 在窗口创建前, 添加如下代码:

        #pragma comment ( lib, "comctl32.lib")

        //程序入口函数
        {
            //...
            InitCommonControls();        //该函数的作用就是注册并初始化新式通用控件窗口类
        }

 

问题到此, 顺利解决! 飞出去的按钮, 又回来了!

 

2. 360, 你这是要闹哪样?

笔者也不是来吐槽或者来黑这个软件的, 为别人免费杀毒, 总之出发点还是好的, 但是当程序从笔者电脑上好好的拿给别人测试时, 结果他反馈给我一张这样的截图:

这感觉就像, 感觉就像啥呢... 这感觉就像是... 算了, 我还是不说脏话了, 做个有底线的人。

也不知这是 CPU 运算出错还是显卡故障造成的, 总之, 他就这样洒脱脱的出现了, 节操何在啊! 当然, 笔者也没有装360进行亲身的实践, 如果有用 360 + TM 组合的您看到这里时, 希望您能下载运行下该程序, 来确认下是否真实存在这种情况。软件 100% 是无毒的, 也没有操作任何其他的进程, 开源项目, 代码都在那挂着, 也不可能会在一个这个小玩意上做手脚。

 




笔者的话

未解决的问题

这是一个不够完善的程序, 有几个功能还没有实现, 例如用键盘模拟拖拽操作, 模拟按下不放开的情况等, 也不能自定义快捷键, 用 Ctrl + 的组合方式导致本软件与其他软件非全局热键的冲突比较厉害, 例如, 在火狐浏览器中使用时, 本程序的 Ctrl + num+ 是指滑轮向下滚动, 但是在火狐中, 他的 Ctrl + num+ 的定义是对网页字体的放大。

此外, 还有一个十分重要的问题, 就是这种模拟是在 API 层面的模拟, 非底层模拟, 如果是在 DirectX 程序中, 该软件是彻底无效的。

结束语

    忙里偷闲学了点 CSS , 排版感觉容易多了。

 

 

--------------------

 

wid, 2013.03.26

 

上一篇: [个人作品] KumquatHandle - IE浏览器缓存监听|分析