《魔高一丈2.0》开发实例
作者:济南 宋悦
一、开发背景:
我想大家都有过忙手忙脚最小化窗口(或关闭窗口)的经历吧!原因很简单——不想让突如其来的老板、老妈、老婆看到我们电脑屏幕上正在显示的游戏、日记、MM:-)
等属于个人隐私的东东。 如果能做一个程序在后台运行,当我们发出一个特殊的输入事件(我选择了鼠标左、右键同时按下)时,该程序就迅速隐藏正在显示的窗口,免去人工瞄准并按下每个窗口右上方的那个小得可怜的的最小化按扭之苦了。当危险解除再利用这个特殊事件使隐藏的窗口恢复。这对于像我这样小脑不太发达、心理素质又不过硬而又经常在老板的眼皮底下“悬崖骑马”的同志们来说是绝对有实战意义的。于是我做了这个“魔高一丈”以实现上述功能!
二、程序原理:
首先,我们得能截获鼠标左、右键同时按下去这个事件——这并不难——设一个标志变量当鼠标发出WM_LBUTTONDOWN并且又有WM_RBUTTONDOWN消息发出时把它置“1”罢了。而我要说明的是,这个“同时按下”只是一种宏观上的概念,鼠标是不会同时发出两个消息的。其次就是解决不管鼠标位于任何窗口之上都能在程序里截获(或者称为监听更准确)到鼠标发出的消息并加以过滤的问题了,这是很关键的。我用了钩子船长的那只钩子(Hook),而且是全局的鼠标钩子,它给了我们跟操作系统沟通的一个机会。许多比较有神秘感的程序(比如金山词霸的鼠标取词)都是用它实现的,稍后我将详细解释。最后就是剩下能得到可见的窗口的句柄(HANDLE)并根据其句柄显示、隐藏窗口的问题了,这也没什么难的有现成的API函数——EnumWindows和ShowWindow。你可以先运行一下我的程序(那个大五星,需要把它跟那个Mousehook.dll文件放在一个文件夹下)。当鼠标左右键一起按下时所有的窗口都隐藏了;再一次同时按下左右键又可恢复隐藏窗口;单击任务栏右下角(托盘)的图标可隐藏或显示本程序窗口。
三、开发步骤:
第0步、选用VC 6.0集成开发环境。
第1步、由于建立全局钩子必须把钩子函数放在DLL里面,所以我们选择MFC AppWizard(DLL)创建一个新的项目,命名为“Mousehook”,再选择选择MFC
Extension DLL类型(为了方便嘛!)。为什么必须把全局钩子函数放在DLL里呢?这是因为系统会动态地调用你所添加的全局鼠标钩子,所有窗口消息数都会由于你添加了鼠标钩子而引起系统处理(何为处理?调用钩子函数也。)这必然需要操作系统能够从一个东东里动态地加入这段处理程序,而这个东东非DLL莫属。
第2步、在项目中加入Mousehook.h文件用以构造一个钩子类——CMousehook,具体如下:
class AFX_EXT_CLASS CMousehook:public CObject
{
public:
CMousehook();
~CMousehook();
BOOL starthook();//封装SetWindowsHookEx( int idHook, HOOK_PROC lpfn, HINSTANCE hMod,DWORD dwThreadID)用来安装钩子
BOOL stophook(); //封装UnhookWindowsHookEx( HHOOK hhk )用来卸载钩子
VOID SetCheck1(UINT i);//处理对话框的选择钩选框1
VOID SetCheck2(UINT i);//处理对话框的选择钩选框2
VOID SetCheck3(UINT i);//处理对话框的选择钩选框3
static BOOL CALLBACK EnumWindowsProc(HWND hwnd,LPARAM lParam);//系统回调的钩子函数
VOID UseForExit();//退出程序时恢复所有隐藏窗口
};
这里我想特别地提一下EnumWindowsProc函数前的CALLBACK跟static,对于CALLBACK我想给大家一个特别江湖的解释其就是:凡是由你设计而却由Windows系统调用的函数,统称callback函数。这些函数都有一定的类型,以配合Windows的调用操作。——引用台湾侯师傅的话。他还说,某些Windows
API函数会要求以callback函数(的函数地址)作为其参数之一。我们这里用到的又比如 SetWindowsHookEx( int idHook,
HOOK_PROC lpfn, HINSTANCE hMod,DWORD dwThreadID)的第二个参数。这种API通常会在进行某种行为之后或满足某种状态的情况下调用其参数中的callback函数。又由于系统在调用callback函数的时候并不会借助任何对象去调用该callback函数,所以在用类来封装callback函数时,需要用static来使callback函数能够独立于对象而又属于类的成员函数。明白了不?(啊?地球人都知道呀!太伤自尊了!)
第3步、在项目中加入Mousehook.cpp文件在CMousehook里封装其中加入必要的共享数据以及SetWindowsHookEx、UnhookWindowsHookEx等函数——这些API函数具体的参数的类型跟作用解释在程序代码的注释里有(网上也到处都有,我也是从网上抠下来的。一个声音高叫着——当然MSDN里也有。),而把它们写在文章里就不免有骗取稿费之嫌了。我只是想解释一下为什么需要使用一个共享的数据段,如下:
#pragma data_seg("mydata") //编译器识别的指令用以在虚拟内存中开辟一个数据段存放该指令下面的数据
HINSTANCE glhInstance=NULL; //DLL实例(或者说模块)的句柄。
HHOOK glhHook=NULL; //鼠标钩子的句柄。
HWND GlobalWndHandle[100]={NULL,.....};//用来存放被隐藏的窗口的句柄,以数组的形式保存。
//该数组必须初始化,原因见下文。我以“......”省略。
UINT Global_i=0; //用以在循环中序列化窗口数组的变量。
BOOL Condition1=0; //用以记录左键按下或释放的标志变量。
BOOL Condition2=0; //用以记录右键按下或释放的标志变量。
BOOL HideOrVisitableFlag=0; //用以标识当再次有左、右键同时按下的情况发生时是隐藏还是显示窗口。
BOOL Check1=0; //用来表示控件Check1状态的标志变量。
BOOL Check2=0; //用来表示控件Check2状态的标志变量。
BOOL Check3=0; //用来表示控件Check3状态的标志变量。
#pragma data_seg() //与#pragma data_seg("mydata") 首尾呼应表示该数据段的结束。
加入上述数据段以后还应在项目里插入一个“Mousehook.def”文件,用:"SECTIONS mydata READ WRITE
SHARED"将mydata数据段设置为一个可读写的共享段。在程序里加入预编译指令,或在开发环境的项目设置里也可以达到设置数据段属性的目的,我就不一一赘述了。
我前面讲过,系统通过调用放在DLL中的钩子回调函数来实现全局钩(钩取所有窗口的鼠标消息),操作系统对DLL的操作仅仅是把DLL映射到需要它的进程的虚拟地址空间里去。也就是说,DLL函数中的代码所创建的任何对象(包括变量)都归调用它的线程或进程所有。“DLL在WIN32中什么都不拥有”——这句话很重要。比如我们在DLL里建立了一个变量a,而我们的这个DLL文件又被两个进程所调用,这两个进程的中都用到了a可这绝对是两个不同存储单元中存储的两个a,它们之间没有丝毫的联系。给其中一个赋值也绝对不会影响到另一个。而对于本程序的一些数据是需要在不同的进程中保持唯一的(也可以说是一致),比方说:
HWND GlobalWndHandle[100]它是用来保存程序做了隐藏的窗口之句柄的数组。当程序运行,我在任意窗口A中同时按下了鼠标左、右键,由于设置了鼠标钩子,系统会调用DLL中的钩子处理函数截获消息并加以处理,即把目前的可见窗口隐藏并把窗口句柄保存到GlobalWndHandle[100]数组中以备将来显示之用。如果不把GlobalWndHandle[100]放到一个共享的数据段里,系统就会在目前我们截获鼠标消息的A窗口的进程的地址空间里开辟HWND
GlobalWndHandle[100]来存储窗口句柄。这样对于其他进程就不能方便地得到这个进程存入GlobalWndHandle[100]数组的数据了。这时只能将GlobalWndHandle[100]等需要跨进程访问的变量数据放在一个共享的数据段里了。另外,需要特别注意——必须给这些变量赋初值(就象我在程序代码里傻呼呼地写了100个NULL一样。你可以不初始化这个数组试验一下,有助于你理解我上面的话),否则编译器会把没有赋初始值的变量放在一个叫未被初始化的数据段中。
第4步:编译生成dll文件,并用MFC AppWizard(exe)建立一个基于对话框的项目,在里面添加一个名为“Mousehook.h”的头文件其内容与dll项目中的“Mousehook.h”文件一致,打开菜单的“Project
Settings”对话框在“Link”选项标签的“Object/library modules”编辑框里填入Mousehook.lib(此文件是与dll一起生成的,当编译一个隐式调用dll的exe时,lib文件起到提供dll引出函数接口地址的作用,如果此路径设置不正确程序是无法进行连接的)文件的存放路径。这样就可以放心使用dll里定义的CMousehook类的成员了。如下:
1 在HideWindowDlg.h中加入#include "MouseHook.h"并在CHideWindowDlg中定义一个CMousehook类对象hook。
2 在CHideWindowDlg::OnInitDialog()函数中加入hook.starthook()并初始化相关变量,这样当对话框初始时就会启动鼠标钩子。
3 在CHideWindowDlg::~CHideWindowDlg()函数中加入hook.stophook()。用以释放对话框对象时解除鼠标钩。
为了不忽略读者的智力水平我只对主要的代码进行了说明,其余有关托盘、Check控件的部分代码都比较传统也没什么好说明的。最后,编译成exe文件以后还须把Mousehook.dll文件拷贝到同exe相同的目录下才能正确运行exe。
临了,希望大家能对我上文中含糊、混沌的地方提出批评指正,也欢迎大家来信(me@sanlian.com.cn)切磋。