看流星社区

 找回密码
 注册账号
查看: 3401|回复: 1

[Delphi] 窗口子类化(SubClassing) SetWindowLong

[复制链接]

该用户从未签到

发表于 2016-9-10 07:24:36 | 显示全部楼层 |阅读模式
一、什么是子类化(Subclass)
视窗系统是基于消息驱动的。因此每一个窗口都有一个函数来处理这些消息,系统管理的窗口结构中有一域记录着这个函数的地址(使用GetWindowLong函数传入索引标识GWL_WNDPROC可获取此值)。所有系统发送到这个窗口的消息都会交由此函数来处理。所以改变窗口结构中这个指针值到自定义的某个函数(使用SetWindowLong函数带GWL_WNDPROC索引标识修改此值),就能接管窗口绝大部分的消息。实际中我们只处理自己感兴趣的某些消息,之后可以有选择的继续Call原来那个窗口函数(使用CallWindowProc函数调用)或直接返回一个值。
二、实现子类化的一般方法
  从上面可以看出,子类化主要使用几个API函数,这几乎是所有实现子类化相同的方法。相关API说明如下:
1 、 WINUSERAPI LONG WINAPI GetWindowLongA(HWND hWnd, int nIndex);
WINUSERAPI LONG WINAPI GetWindowLongW(HWND hWnd, int nIndex);
2 、 SetWindowLong 与 CallWindowProc 函数这不再列出来了,都是粉容易的!
使用 ComCtl32.dll version 6 实现窗口子类化
  Windows XP 带的ComCtl32.dll version 6 提供了4个可以让创建子类化更简单,并且可以消除前面提到的缺陷的函数。这些新的函数封装了对多组参考数据(multiple sets of reference data)的管理操作,使得开发者能将精力集中到具体的程序特性而不是对子类的管理上。这些新的函数为:
SetWindowSubclass
GetWindowSubclass
RemoveWindowSubclass
DefSubclassProc
下面是对这些函数的描述。
SetWindowSubclass
这个函数用来子类化一个窗口。每个子类可以用p pfnSubclass 和 uIdSubclass (SetWindowSubclass的参数)唯一标识。多个子类可以共享同一个子类过程,而用标识(ID)来区分。改变参考数据可以提供再一次调用SetWindowSubclass 来实现。一个重要的优点是每一个子类实例可以拥有自己的参考数据。
子类过程的声明和传统的窗口过程有点细微的差别,它多了两个参数:子类ID和参考数据。参看下面这个函数声明的最后两个参数:
LRESULT CALLBACK MyWndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass,
   DWORD_PTR dwRefData);
每次新的窗口过程收到一个消息时,它同时得到了一个子类ID和参考数据。
注意:所有作为参数传递给该函数的字符串均为Unicode,不管有没有定义Unicode编译选项。
GetWindowSubclass
该函数取回一个子类的信息。比如你可以用GetWindowSubclass 来访问参考数据。
RemoveWindowSubclass
该函数移除一个子类.RemoveWindowSubclass 和 SetWindowSubclass 联合使用可以动态添加和删除子类.
DefSubclassProc
这个函数调用子类链中的下一个处理者。这个函数可以自行取得正确的ID和参考数据并传递给下一个窗口过程。
大家都知道每个窗口都有默认的窗口函数来进行对窗口消息的处理.
而子类化技术就是替换窗口的窗口函数为自己定义的函数的技术.例如下面的代码:
var
   Form1: TForm1;
   OldWndProc: Pointer;
implementation
{$R *.dfm}

function NewWndProc(hHwnd, Msg, wParam, lParam: LongWORD): Longint; stdcall;
begin
   if Msg = WM_CLOSE then
      exit;
   Result := CallWindowProc(OldWndProc, hHwnd, Msg, wParam, lParam);
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
{保存旧的窗口函数地址}
   OldWndProc := Pointer(GetWindowLong(Self.Handle, GWL_WNDPROC));
{设置新的窗口函数为自定义函数}
   SetWindowLong(Self.Handle, GWL_WNDPROC, Longint(@NewWndProc));
end;
这样在窗口建立时就对窗口实现了子类化, 这时按下窗口的关闭按钮就会发现关不了窗口, 因为新的窗口处理函数把WM_CLOSE消息过滤掉了, 要取消子类化, 只需要简单的把以前的窗口函数恢复过来就可以了.SetWindowLong(Self.Handle, GWL_WNDPROC, Longint(OldWndProc));
现在看来似乎很简单, 只要对其它进程中的目标窗口进行子类化就可以实现对其消息的拦载监视了.但是在WIN32下, 每一个进程都有自己独立的内存空间, 新的窗口函数必须和目标窗口在同一个进程内, 直接使用SetWindowLong(其它进程中窗口的句柄, GWL_WNDPROC, 新窗口函数)就会失败, 所以就要想办法把我们的窗口函数代码放到目标进程内, 这儿有二个办法, 一是使用CreateRemoteThread在目标进程内建立线程, 但这函数只在NT及以上操作系统实现, 而且还要涉及到API地址重定位等问题, 很麻烦(请参考http: //www.csdn.net/develop/Read_Article.asp?Id=21079).另一个方法就是使用HOOK技术(SetWindowsHookEx,如果不知道,请先参考HOOK技术方面的文章),大家都知道,对其它进程进行HOOK时,此进程会自动加载HOOK过程所在的DLL,如果我们把窗口函数也放在DLL中,那窗口函数就相当于加载到了目标进程的地址空间中了,这方法简单易行.在这里我们就采用HOOK技术来实现跨进程子类化.
   最后一个问题是如何在DLL中实现全局变量, 因为DLL中的变量在每个进程加载这个DLL时都申请新的空间来存放变量, 所以DLL中的变量在各个进程内不一样, 可以利用内存文件映射, WM_COPYDATA等方法来实现全局变量.这儿采用内存文件映射.
   现在需要的知识都已了解了, 就让我们来看具体的代码吧(这儿是把所有函数放在一个DLL中):
   library Hook;
   uses
   SysUtils, windows, Messages;
   const
   WM_UNSUBCLASS = WM_USER 1001; {卸载子类化消息}
   WM_NEWMESSAGE = WM_USER 1002; {通知监视窗口拦到了新消息}
   HOOK_EVENT_NAME = 'MyHook';
   type
   PMyDLLVar = ^TMyDLLVar;
   TMyDLLVar = record
      SubClass: Boolean; {是否已经子类化}
      HookWindow, SpyWindow: LongWORD; {要安装HOOK的窗口及用于接收消息的窗口}
      hHook: LongWORD; {HOOK句柄}
      OldWndProc: pointer; {旧的窗口过程}
      MsgHwnd: LongWORD;
      Msg: TMessage;
   end;
   var
   DLLData: PMyDLLVar;
{---------------------------------------}
{函数名:NewWndProc
{函数功能:新的窗口过程
{函数参数:hHwnd:窗口句柄 Msg:消息ID
{ wParam, lParam:消息参数
{函数返回值:下一个窗口过程的返回值
{---------------------------------------}

function NewWndProc(hHwnd, Msg, wParam, lParam: LongWORD): Longint; stdcall;
begin
   if Msg = WM_UNSUBCLASS then {如果收到卸载子类化消息就恢复以前的WndProc}
   begin
      SetWindowLong(DLLData^.HookWindow, GWL_WNDPROC, longint(DLLData^.OldWndProc));
      exit;
   end;
{这儿是把收到的消息放在映射的内存中,我们自己的程序可以通过读这个内存来得到监视到的消息.}
   DLLData^.Msg.Msg := Msg;
   DLLData^.Msg.WParam := wParam;
   DLLData^.Msg.LParam := lParam;
   DLLData^.MsgHwnd := hHwnd;
{给监视窗口发送拦载新消息的消息}
   SendMessage(DLLData^.SpyWindow, WM_NEWMESSAGE, 0, 0);
{这儿可以添加自己对目标进程消息处理的代码,因为己经是在目标进程的地址空间内,现在可以为所
欲为 ^_^}
Result := CallWindowProc(DLLData^.OldWndProc, hHwnd, Msg, wParam, lParam);
end;
{------------------------------------}
{过程名:HookProc
{过程功能:HOOK过程
{过程参数:nCode, wParam, lParam消息的相
{ 关参数
{------------------------------------}

procedure HookProc(nCode, wParam, lParam: LongWORD); stdcall;
var
   hEvent: THandle;
begin
   if not DllData^.SubClass then {如果此窗口未子类化}
   begin {保存窗口过程地址并子类化}
      if hEvent <> 0 then
      begin
         WaitForSingleObject(hEvent, INFINITE);
         CloseHandle(hEvent);
      end;
      DLLData^.OldWndProc := pointer(GetWindowLong(DLLData^.HookWindow, GWL_WNDPROC));
      SetWindowLong(DLLData^.HookWindow, GWL_WNDPROC, integer(@NewWndProc));
      DLLData^.SubClass := True;
      hEvent := OpenEvent(Synchronize, False, HOOK_EVENT_NAME);
   end;
{调用下一个Hook}
   CallNextHookEx(DLLData^.hHook, nCode, wParam, lParam);
end;

{------------------------------------}
{函数名:InstallHook
{函数功能:在指定窗口上安装HOOK
{函数参数:HWindow:要安装HOOK的窗口
{ SWindow:用于接收消息的窗口
{返回值:成功返回TRUE,失败返回FALSE
{------------------------------------}

function InstallHook(HWindow, SWindow: LongWORD): Boolean; stdcall;
var
   ThreadID: LongWORD;
   hEvent: THandle;
begin
   Result := False;
   DLLData^.hHook := 0;
   DLLData^.HookWindow := HWindow;
   DLLData^.SpyWindow := SWindow;
{得到指定窗口的线程ID}
   ThreadID := GetWindowThreadProcessId(HWindow, nil);
{给指定窗口挂上钩子}
   hEvent := CreateEvent(nil, True, False, HOOK_EVENT_NAME);
   DLLData^.hHook := SetWindowsHookEx(WH_GETMESSAGE, @HookProc, Hinstance, ThreadID);
   SetEvent(hEvent);
   CloseHandle(hEvent);
   if DLLData^.hHook > 0 then Result := True; {是否成功HOOK}
end;
{------------------------------------}
{过程名:UnHook
{过程功能:卸载HOOK
{过程参数:无
{------------------------------------}

procedure UnHook; stdcall;
begin
{发送卸载子类化消息给指定窗口}
   SendMessage(DLLData^.HookWindow, WM_UNSUBCLASS, 0, 0);
   DLLData^.SubClass := False;
{卸载Hook}
   UnhookWindowsHookEx(DLLData^.hHook);
end;
{------------------------------------}
{过程名LL入口函数
{过程功能:进行DLL初始化,释放等
{过程参数LL状态
{------------------------------------}

procedure MyDLLHandler(Reason: Integer);
var
   FHandle: LongWORD;
begin
   case Reason of
      DLL_PROCESS_ATTACH:
         begin {建立文件映射,以实现DLL中的全局变量}
            FHandle := CreateFileMapping($FFFFFFFF, nil, PAGE_READWRITE, 0, $FF, 'MYDLLDATA');
            if FHandle = 0 then
               if GetLastError = ERROR_ALREADY_EXISTS then
               begin
                  FHandle := OpenFileMapping(FILE_MAP_ALL_ACCESS, False, 'MYDLLDATA');
                  if FHandle = 0 then Exit;
               end else Exit;
            DLLData := MapViewOfFile(FHandle, FILE_MAP_ALL_ACCESS, 0, 0, 0);
            if DLLData = nil then
               CloseHandle(FHandle);
         end;
      DLL_PROCESS_DETACH:
         if Assigned(DLLData) then
         begin
            UnmapViewOfFile(DLLData);
            DLLData := nil;
         end;
      DLL_THREAD_ATTACH: ;
      DLL_THREAD_DETACH: ;
   end;
end;
{$R *.res}
exports
   InstallHook, UnHook, HookProc;
begin
   DLLProc := @MyDLLHandler;
   MyDLLhandler(DLL_PROCESS_ATTACH);
end.
编译这个DLL, 然后在我们的程序中加载这个DLL, 并调用InstallHook(目标窗口句柄, 自己窗口句柄)就可 以实现对目标窗口消息的监视了(在接收到WM_NEWMESSAGE消息时读映射的内存), 调用UnHook则可以卸载掉子类化和HOOK.具休的代码还请读者自行编写.

该用户从未签到

发表于 2018-9-21 12:04:30 | 显示全部楼层
你好,我看了你的代码。比其他人说的很详细,我想截取我另外一个程序的edit失去焦点的消息,那么我拦截消息的时候能不能不去dll里面做,失去焦点写个简单的例子怎么写,比如我有一个Form2 ,里面有edit1、edit2、edit3,我想监控edit1和edit2两个文本框失去焦点事件,代码写在我的Form1里面
点击按钮快速添加回复内容: 支持 高兴 激动 给力 加油 苦寻 生气 回帖 路过 感恩
您需要登录后才可以回帖 登录 | 注册账号

本版积分规则

小黑屋|手机版|Archiver|看流星社区 |网站地图

GMT+8, 2024-3-19 15:23

Powered by Kanliuxing X3.4

© 2010-2019 kanliuxing.com

快速回复 返回顶部 返回列表