我们写程序,常将完成一个特定功能的代码写到一个函数中,以后直接调用便可实现该功能。对于宏,其实也是相同的原理,将一些键盘和鼠标的操作“录制”起来,以后如果要重复这些操作,只需要将这些操作“回放”就行了。许多编辑软件都有宏的功能,比如Word。Delphi也有,按Ctrl+Shift+R进行宏录制,再按Ctrl+Shift+R结束,最后按Ctrl+Shift+P即可回放刚才的输入操作。宏的实现正是钩子的一个应用,使用WH_JOURNALRECORD和WH_JOURNALPLAYBACK钩子可以完成对于键盘来鼠标动作的“录制”和“回放”,基于此技术,我们就来实现一个自己的宏。
大多数软件的宏只是记录键盘的操作,并没有鼠标的操作,但有时候记录鼠标的操作也是有好处的,比如你正在测试一个程序,需要重复地进行点击移动,这时鼠标的宏就派上用场了。前一阵子一个朋友用VC实现了一个UI自动测试工具,非常有趣,这次我也依照它用Delphi来实现一个,它可以记录所有的鼠标键盘操作,并回放出来,还可以保存这些操作。
先说日志钩子,日志钩子不需要动态链接库*.DLL,就能实现系统级的事件监控,它只能监视两种事件,即鼠标,键盘的操作。我们用前一节所说的API:SetWindowsHookEx安装一个日志钩子,这里要重点说明日志钩子的过滤函数:
LRESULT CALLBACK JournalRecordProc(
int code, // 钩子编码
WPARAM wParam, // 没有使用
LPARAM lParam // 被处理的消息
);
其中Code说明如何处理消息,可以是如下的值:
HC_ACTION:lParam是指向一个EVENTMSG结构的指针,该结构包含了从系统队列中移除的消息的信息。
HC_SYSMODALOFF :一个系统模态对话框(如注销对话框)已经被销毁的时候,该钩子过程重新开始记录。
HC_SYSMODALON:一个系统模态对话框正在被销毁,直到它被销毁之前,钩子过程停止记录。
如果Code参数小于0,则要像第一部分所说的调用CallNextHookEx函数,并返回它的返回值。
lParam参数是指向一个EVENTMSG结构的指针。这个结构就是我们要记录的信息。
EVENTMSG如下声明:
typedef struct tagEVENTMSG {
UINT message; //指定消息
UINT paramL; //指定消息的附加信息,该信息取决于消息的值
UINT paramH; //指定消息的附加信息,该信息取决于消息的值
DWORD time; //消息传递的时间
HWND hwnd; //消息将传递到的窗口的句柄。
} EVENTMSG, *PEVENTMSG;
在这里给出paramL和paramH对鼠标和键盘的意义:
ParamL:如果是键盘消息则代表虚拟码,如果是鼠标消息则代表坐标X
ParamH:如果是键盘消息则代表击键的扫描码,如果是鼠标消息则代表坐标Y。
我们还要有一个数据结构来保存传递到日志过程中的事件结构列表,以便在回放钩子中使用。Delphi为我们提供了TList,非常方便,List的每一项存储一个EVENTMSG结构指针。不过TList并不管理事件结构指针的生命周期,为方便计,我们可以继续一个子类,并覆盖它的相应方法,即可实现指针的自动释放。
最后还要能将消息序列化,保存到文件中,以后可以重新加载到TList中使用。这里就称为保存和打开的功能。XML是一个不错的选择,可以用XML来保存事件结构(关于XML的使用,请查看其他资料)。
日志记录钩子的技术准备就这些,下面是日志回放钩子的一些知识。
下面是回放钩子的过滤函数:
LRESULT CALLBACK JournalPlaybackProc(
int code, // 钩子编码
WPARAM wParam, // 没有使用
LPARAM lParam // 被处理的消息
);
当回放钩子安装之后,常规的鼠标和键盘输入就无效了。注意这里的常规是指还有一些不常规的按键有效,比如Ctrl+Alt+Del。
Code指定了钩子过程如何处理消息,它可以是如下的值:
HC_GETNEXT:如果准备好了(这里的准备好与否由HC_SKIP决定),则将当前鼠标和键盘消息拷贝到LParam参数中,对于我们的程序,就是将事件结构列表中的一个事件结构拷贝给Lparam。
HC_SKIP:钩子过程必须准备将下一个鼠标和键盘拷贝到Lparam参数指向的EventMsg结构中。当为这个值时,表示已经准备好了,我们可以设一个全局变量,在这里标识为真,则在HC_GETEXT时,判断该全局变量 ,如果为真,则将事件拷贝给LParam
HC_SYSMODALOFF和HC_SYSMODALON:和记录钩子相似。
回放钩子过程的返回值也必须注意,当Code值为HC_GETNEXT时,它指定系统处理当前消息之前要等多少时间,这个时间以毫秒为单位。我们可以通过当前事件结构中的时间与前一个事件结构的时间计算出这个时间值。如果Code是其他值,返回值忽略。
这里补充一点知识, Windows有一个MSG结构,其中有一个成员是Time,表示消息发送的时间,这个时间其实是个相对的值,它从操作系统启动时开始以毫秒为单位增加。调用GetTickCount可以获得该值。
有一种情况,就是当我们按下Alt+Ctrl+Del时,钩子会停止下来的,这个时候有什么消息来通知程序,让我们可以做一些设置工作呢,幸好系统为我们准备了WM_CANCELJOURNAL消息,记录和回放钩子在工作时,当按下那些键,系统发送该消息给应用程序的消息循环,通知钩子停止了。我们可以取得Application的OnMessage事件,在其中判断,如果获得了该消息,则作一些变量的设置操作。
好了,还是以代码来说明吧。
是否给出全部源代码,我考虑了一下,如果代码过长,则怕有臭婆娘的缠脚布之嫌。但如果不全部给出,则又怕理解不全。最后还是狠一下心,把全部代码贴出来,都封在wdMacro单元中,读者可以直接拷贝到工程中,再调用其中的API即可:
钩子及其应用(三)
unit wdMacro;
{*******************************************
* brief: 日志钩子实现宏功能
* autor: linzhenqun
* date:
* email: linzhengqun@163.com
* blog: http://blog.csdn.net/linzhengqun
********************************************}
interface
uses
Windows, Messages, Classes, SysUtils;
type
{回放可以调速度的哦!}
TPlaySpeed = (psFastest, psFaseter, psNormal, psSlower, psSlowest);
{录制和回放完毕的回调函数}
TSimpleProc = procedure;
{开始记录事件}
function StartRecord: Boolean;
{停止刻录事件}
function StopRecord: Boolean;
{开始回放事件}
function StartPlayBack(PlaySpeed: TPlaySpeed): Boolean;
{停止回放事件}
function StopPlayBack: Boolean;
{保存事件}
function SaveEventList(FileName: string): Boolean;
{打开事件列表}
function OpenEventList(FileName: string): Boolean;
{系统动作使得钩子停止}
procedure HookStopBySystem(Msg: LongWord);
var
RecordStop: TSimpleProc;
PlayStop: TSimpleProc;
implementation
uses
Math, XMLDoc, xmldom;
const
Max_EventNum = 1000000; //一百万个消息足矣
type
{管理事件结构指针,负责销毁它们}
TEventList = class(TList)
public
{覆盖该方法,释放指针的内存}
procedure Notify(Ptr: Pointer; Action: TListNotification); override;
end;
var
EventList: TEventList; //事件结构列表
HRecord: THandle; //记录钩子的句柄
HPlay: THandle; //回放钩子的句柄
Recording: Boolean; //标识是否正在记录
Playing: Boolean; //标识是否在回放
EventIndex: Integer; //当前回放的事件索引
IsReady: Boolean; //准备好拷贝了吗。
Speed: Integer; //回放速度,小于0表示正确速度
{ TEventList }
procedure TEventList.Notify(Ptr: Pointer; Action: TListNotification);
begin
inherited;
if (Action = lnDeleted) and (Ptr <> nil) then
Dispose(Ptr);
end;
{internal procedure}
function GetPlaySpeed(PlaySpeed: TPlaySpeed): Integer;
begin
case PlaySpeed of
psFastest: Result := 0;
psFaseter: Result := 5;
psNormal: Result := -1;
psSlower: Result := 50;
else Result := 80;
end;
end;
{ Hook proc }
function RecordProc(nCode: integer; wParam: WPARAM;
lParam: LPARAM): LRESULT; stdcall;
var
PEvent: PEventMsg;
begin
case nCode of
HC_ACTION:
begin
if EventList.Count >= Max_EventNum then
StopRecord
else begin
new(PEvent);
Move(PEventMsg(lParam)^, PEvent^, SizeOf(TEventMsg));
EventList.Add(PEvent);
end;
end;
end;
Result := CallNextHookEx(HRecord, nCode, wParam, lParam);
end;
function PlayBackProc(nCode: integer; wParam: WPARAM;
lParam: LPARAM): LRESULT; stdcall;
begin
Result := 0;
case nCode of
HC_SKIP:
begin
Inc(EventIndex);
if EventIndex >= EventList.Count then
begin
StopPlayBack;
IsReady := False;
end
else
IsReady := True;
end;
HC_GETNEXT:
begin
if IsReady then
begin
IsReady := False;
if Speed < 0 then
Result := PEventMsg(EventList.Items[EventIndex])^.time -
PEventMsg(EventList.Items[EventIndex - 1])^.time
else
Result:= Speed;
end
else
Result := 0;
PEventMsg(lParam)^ := TEventMsg(EventList.Items[EventIndex]^);
end;
else
Result := CallNextHookEx(HPlay, nCode, wParam, lParam);
end;
end;
{ save event to xml}
//将事件结构列表保存到XML文件中
function SaveEventListToXML(AEventList: TEventList; AXMLDoc: TXMLDocument): Boolean;
var
i: Integer;
RootNode, ParenNode: IDOMNode;
temStr: string;
{初始化XML文档}
procedure InitXMLDoc;
begin
AXMLDoc.XML.Text := '';
AXMLDoc.Active := True;
AXMLDoc.Encoding := 'utf-8';
end;
{在一个父结点下增加一个子结点}
function ApendNode(PNode: IDOMNode; tagName, Value: WideString): IDOMNode;
var
CNode: IDOMNode;
TextNode: IDOMText;
begin
with AXMLDoc.DOMDocument do
begin
CNode := createElement(tagName);
if Value <> '' then
begin
TextNode := createTextNode(Value);
CNode.appendChild(TextNode);
end;
Result := PNode.appendChild(CNode);
end;
end;
begin
Result := False;
if AEventList.Count = 0 then
Exit;
try
InitXMLDoc;
RootNode := AXMLDoc.DOMDocument.createElement('EventList');
AXMLDoc.DOMDocument.documentElement := IDOMElement(RootNode);
for i := 0 to AEventList.Count - 1 do
begin
ParenNode := ApendNode(RootNode, 'EventMsg', '');
temStr := IntToStr(TEventMsg(EventList.Items[i]^).message);
ApendNode(ParenNode, 'Message', temStr);
temStr := IntToStr(TEventMsg(EventList.Items[i]^).paramL);
ApendNode(ParenNode, 'ParamL', temStr);
temStr := IntToStr(TEventMsg(EventList.Items[i]^).paramH);
ApendNode(ParenNode, 'ParamH', temStr);
temStr := IntToStr(TEventMsg(EventList.Items[i]^).time);
ApendNode(ParenNode, 'Time', temStr);
temStr := IntToStr(TEventMsg(EventList.Items[i]^).hwnd);
ApendNode(ParenNode, 'Hwnd', temStr);
end;
Result := True;
except
//什么也不做
end;
end;
//从XML文件中加载事件结构列表
function GetEventListFromXML(AEventList: TEventList; AXMLDoc: TXMLDocument): Boolean;
var
i: Integer;
PE: PEventMsg;
function GetNodeValue(ANode: IDOMNode): Integer;
begin
Result := StrToInt(ANode.firstChild.nodeValue);
end;
begin
Result := False;
try
with AXMLDoc.DOMDocument.documentElement do
for i := 0 to childNodes.length - 1 do
begin
new(PE);
PE^.message := GetNodeValue(childNodes[i].childNodes[0]);
PE^.paramL := GetNodeValue(childNodes[i].childNodes[1]);
PE^.paramH := GetNodeValue(childNodes[i].childNodes[2]);
PE^.time := GetNodeValue(childNodes[i].childNodes[3]);
PE^.hwnd := GetNodeValue(childNodes[i].childNodes[4]);
EventList.Add(PE);
end;
Result := True;
except
end;
end;
{ macro API }
function OpenEventList(FileName: string): Boolean;
var
XMLDoc: TXMLDocument;
begin
Result := False;
XMLDoc := TXMLDocument.Create(nil);
try
EventList.Clear;
XMLDoc.LoadFromFile(FileName);
if GetEventListFromXML(EventList, XMLDoc) then
Result := True;
finally
XMLDoc.Free;
end;
end;
function SaveEventList(FileName: string): Boolean;
var
XMLDoc: TXMLDocument;
begin
Result := False;
XMLDoc := TXMLDocument.Create(nil);
try
if SaveEventListToXML(EventList, XMLDoc) then
begin
XMLDoc.SaveToFile(FileName);
Result := True;
end;
finally
XMLDoc.Free;
end;
end;
function StartPlayBack(PlaySpeed: TPlaySpeed): Boolean;
begin
Result := False;
if Recording or Playing then
Exit;
if EventList.Count = 0 then
Exit;
EventIndex := 0;
Speed := GetPlaySpeed(PlaySpeed);
HPlay := SetWindowsHookEx(WH_JOURNALPLAYBACK, @PlayBackProc, HInstance, 0);
Result := HPlay <> 0;
Playing := Result;
end;
function StartRecord: Boolean;
begin
Result := False;
if Playing or Recording then
Exit;
EventList.Clear;
HRecord := SetWindowsHookEx(WH_JOURNALRECORD, @RecordProc, HInstance, 0);
Result := HRecord <> 0;
Recording := Result;
end;
function StopPlayBack: Boolean;
begin
Result := False;
if not Playing or Recording then
Exit;
Result := UnhookWindowsHookEx(HPlay);
if Result then
begin
if Assigned(PlayStop) then
PlayStop();
Playing := False;
end;
end;
function StopRecord: Boolean;
begin
Result := False;
if not Recording or Playing then
Exit;
Result := UnhookWindowsHookEx(HRecord);
if Result then
begin
Recording := False;
//通知外部,记录已经停止
if Assigned(RecordStop) then
RecordStop();
end;
end;
procedure HookStopBySystem(Msg: LongWord);
begin
if Msg = WM_CANCELJOURNAL then
begin
if Playing then
begin
Playing := False;
if Assigned(PlayStop) then
PlayStop();
end
else if Recording then
begin
Recording := False;
if Assigned(RecordStop) then
RecordStop();
end;
end;
end;
initialization
EventList := TEventList.Create;
finalization
EventList.Free;
end.
没有想到代码一贴竟是这么多,实在不敢将界面的代码贴出来了,读者可以自己建一个界面调用其中的函数。
这里有几点要说明,以免新手迷惑:
1:RecordStop: TSimpleProc;
PlayStop: TSimpleProc;
有这两个全局的回调函数,当停止记录和播放时,会调用它们,如果外面单元有赋值的,则可以在这个事件中作一些操作,比如提示信息。
2:关于那个TList的子类,当List增加删除一项时,会调用Notify方法,这个方法是一个虚方法,子类可以覆盖它,EventList子类即覆盖它,并判断如果是删除操作时,把指针给销毁掉,这样就免去我们的很多的麻烦。有兴趣可以读一读VCL的源码,则更加明白了。
3:SaveEventListToXML和GetEventListFromXML是两个操作XML文件的函数,其中的方法与这篇文章无关,所以这里就不想再去作什么解释了,不然又要一大篇,有兴趣者去看一看关于XML的应用吧,或者有机会我再写一篇文章说明之。
4:{系统使得钩子停止}
procedure HookStopBySystem(Msg: LongWord);这一过程是当你按下Ctrl+Alt+Del时系统发送WM_CANCELJOURNAL给应用程序时,程序的处理过程调用的。比如外部有了Application的OnMessage的处理函数,当处理到WM_CANCELJOURNAL时,则调用HookStopBySystem作一些操作。

浙公网安备 33010602011771号