DELPHI 跨进程获取其他程序的DBGrid内容
发布时间:2020-12-15 10:03:20 所属栏目:大数据 来源:网络整理
导读:一、思考与启发? ?1.对于Windows的Standard Controls,基本上大家应该是都会的: ? GetWindowText,WM_GETTEXT可以获取EDIT的文本内容; ? LB_GETTEXT可以获取LISTBOX列表项的文本内容; ? CB_GETLBTEXT可以获取COMBOBOX下拉列表项的文本内容; ? 这里我就不
一、思考与启发?
?1.对于Windows的Standard Controls,基本上大家应该是都会的:
? GetWindowText,WM_GETTEXT可以获取EDIT的文本内容; ? LB_GETTEXT可以获取LISTBOX列表项的文本内容; ? CB_GETLBTEXT可以获取COMBOBOX下拉列表项的文本内容; ? 这里我就不多说了。 ? 对于Windows的Common Controls,如LISTVIEW、TREEVIEW等,在本进程自身中获取的话可以直接用LVM_GETITEMTEXT,TVM_GETITEM消息,跨进程的话,还需要另外用到一些API函数,有兴趣的可以看看这个贴子: ?? http://www.delphibbs.com/delphibbs/dispq.asp?lid=3224504 ? DBGrid是Delphi的自写控件,不是Windows控件,没有什么消息可以利用来获取它所显示的数据记录内容。 2.运用鼠标屏幕取词的技术,可以获取到鼠标位置的显示内容,这个是采用的ApiHook技 ? 术,截获屏幕输出函数,从而得到输出内容,其实现原理这里我不想多说,delphibbs ? 论坛上相关的贴子很多,大家搜索一下就能找到。不过这种方法的实用性不很强,因为 ? 它需要在鼠标位置使输出重画,才能截获到输出内容的;而且如果DBGrid显示不完整有 ? 滚动条的情况下,没有显示的字段、记录的内容就不能截获到。 3.在本进程自身中,是可以获得DBGrid显示的记录内容的,如 ? DBGrid1.Columns[1].Field.DisplayText,不过前提是要得到对象实例DBGrid1,假如 ? 我们只知道DBGrid1的句柄。这个当然也没有什么问题,FindControl函数可以完成这个 ? 功能。 4.上面说到,只要能够得到DBGrid的对象实例,我们就能够获得它的内容啦。前面讲的是 ? 在本进程可以获取到DBGrid内容,而我们现在要讲的是跨进程获取其他程序的DBGrid??? 内容,那么是不是真的能够得到其他进程的DBGrid对象实例呢?如果能够的话,又该怎么 ? 实现呢?另外,还有一个问题,我们获取到了这个DBGrid的对象实例,但是这个地址是 ? 其他进程地址空间中的一个虚拟地址,我们能够DBGrid1.Columns[1].Field.DisplayText ? 这样在自己的进程中访问获取其内容吗? 二、必备知识基础? ?1.进程地址空间? ? Win32系统,所有32位应用程序都有4GB的进程地址空间(32位地址最多可以映射4GB的内存)。应用程序可以访问2GB的进程地址空间,称为用户模式虚拟地址空间。应用程序拥有的所有线程都共享同一个用户模式虚拟地址空间。其余2GB为操作系统保留(也称为内 ? 核模式地址空间)。? ? 而从Win2000 Server开始的所有操作系统版本,还有一个boot.ini ? 开关,可以为应用程序提供访问3GB的进程地址空间的权限,从而将内核模式地址空间 ? 压缩为1GB。一般地,一个用户进程不可以直接访问另外一个用户进程的地址空间。进 ? 程地址空间的描述请参考? http://msdn2.microsoft.com/zh-cn/library/ms189334.aspx ? 和《Windows核心编程》的第13章 Windows的内存结构。?? ? 顺便说一句,《Windows核心编程》是一本相当经典的好书,呵呵,建议大家都应该购 ? 买收藏一本,认认真真地把它看几遍。题外话,呵呵?? ?2.DLL基础? ?1)先说说为什么要使用DLL? ?a.它可以动态装载,这样不必要在应用程序初始化就装载所有的代码,可以根据需要、操 ? 作再装载DLL,这样启动速度比较快,也更节省内存; b.便于项目管理,不同的开发人员、开发小组在不同的模块上工作; c.有助于解决操作系统平台的差异,比如98/2000/XP等枚举进程可以用 ? CreateToolhelp32Snapshot,Process32First,Process32Next,而NT上就不能用,而要 ? 用psapi.dll中的EnumProcesses,EnumProcessModules,GetModuleFileNameEx等函数; d.可以用多种编程语言编写,开发人员可以选用自己最擅长的语言; e.有助于资源的共享和应用程序的本地化,DLL可以包含对象框模板、图标、字符串、位 ? 图等资源;? 打错了,是对话框模板? ?f.可以实现一些特殊的目的,如系统范围的全局钩子,就要求写在DLL中才行。 2)EXE程序的全局变量不能被同一个EXE程序的多个运行实例所共享,DLL中的全局变量的处理方法也是一样的。也就是说,当一个进程将一个DLL映射到它的地址空间中去的时 ?候,系统会同时创建全局变量的实例。? 也就是说不同进程间用的同一个DLL,全局变量的值可能是不一样的,不能共享? ?3)DLL和EXE之间的数据共享,这有很多技术,全局原子、内存映射、WM_COPYDATA消息等, ? 这个不是本文的重点,这里我们就不一一赘述。? ?4)Delphi写DLL要注意的问题:? 这一点我们需要注意?? ?a.参数和返回值为string、动态数组类型时,DLL和EXE都要把ShareMem作为.dpr工程的第一个单元引用。当然最好是不要使用string、动态数组类型,可以改用PChar、数组指 ? 针类型,如果是混合语言编程使用的话,就一定不能用string、动态数组类型。这样做 ? 的原因是DLL和EXE的内存管理器(MemoryManager)不是一个,而string、动态数组类型是通过引用计数由Delphi自动进行内存管理的,它何时分配何时释放,我们不能显式的 ? 知道的,DLL分配而EXE释放的话,这样就出问题了。用ShareMem就是为了让它们统一使用一个内存管理器进行内存分配释放。? 关于DLL和EXE内存管理器不同这一点,非常重要!!!? ?b.DLL和EXE的VCL类体系不是一个,它们各自有一套,因此,从EXE传递过去的对象,要在DLL中用is判断类型和as作类型转换,那都不能得到期望的结果。? 如果是bpl,就不会有这个问题?? ?c.DLL中应用ADO、窗体(模态、非模态、MDI子窗体)、线程等的一些相关问题与今天的主题关系不大,就不作多讲了。参考 ?? http://www.delphibbs.com/delphibbs/dispq.asp?LID=2977902 ?? http://www.delphibbs.com/keylife/iblog_show.asp?xid=2438 ?? http://www.delphibbs.com/keylife/iblog_show.asp?xid=11558?等? 关于DLL的详细论述请参考《Windows核心编程》第19、20章。? ?3.钩子(Hook)? ? Windows系统是建立在事件驱动的机制上的,说穿了就是整个系统都是通过消息的传递 ? 来实现的。而钩子是Windows系统中非常重要的系统接口,用它可以截获并处理送给其 ? 他应用程序的消息,来完成普通应用程序难以实现的功能。钩子可以监视系统或进程中 ? 的各种事件消息,截获发往目标窗口的消息并进行处理。这样,我们就可以在系统中安 ? 装自定义的钩子,监视系统中特定事件的发生,完成特定的功能,比如截获键盘、鼠标 ? 的输入,屏幕取词,日志监视等等。 ? 按事件分类,有如下的几种常用类型的钩子: ? 1)键盘钩子可以监视各种键盘消息。 ? 2)鼠标钩子可以监视各种鼠标消息。 ? 3)外壳钩子可以监视各种Shell事件消息。 ? 4)日志钩子可以记录从系统消息队列中取出的各种事件消息。 ? 5)窗口过程钩子监视所有从系统消息队列发往目标窗口的消息。? ? 安装钩子:SetWindowsHookEx ? 卸载钩子:UnhookWindowsHookEx ? 钩子回调函数形式: function GetMsgProc(Code: UINT; lParam: LPARAM; wParam: WPARAM): LRESULT; stdcall;? ? 系统全局钩子必须在DLL中,因为它影响系统的所有应用程序,需要在消息发生时被系 ? 统映射到其他进程的地址空间,从而调用DLL中的钩子回调函数。钩子所在的DLL被映射时,是整体映射被加载到被挂钩的进程的地址空间中,而不仅仅是钩子回调函数,这 ? 样,被挂钩的进程就可以访问DLL中的变量和调用其他函数的。利用这个特点,在应用 ? 中就可以做到很多特定的功能,比如屏幕取词、木马、三级跳隐藏进程等。 注意:安装了某类消息的系统全局钩子之后,在该类消息发生时钩子DLL会被系统映射到其他进程的地址空间,从而调用DLL中的钩子回调函数。? 还有一点要注意:当SetWindowsHookEx调用成功后,系统会自动映射这个DLL到被挂钩 ? 的线程,但并不是立即映射。因为所有的Windows钩子都是基于消息的,直到一个适当 ? 的事件发生后这个DLL才被映射。同理,UnhookWindowsHookEx调用之后,也是在某个适当的事件发生之后DLL才真正地从被挂钩线程卸载。? ?function GetMsgProc(Code: UINT; lParam: LPARAM; wParam: WPARAM): LRESULT; stdcall;? 能介绍这个回调函数什么用吗? 必备的知识基础就讲这么多,有什么问题待会儿我们大家再探讨? ?Code:钩子代码,通常为HA_ACTION时是用户要处理的? ?lParam,wParam:跟具体安装的钩子类型有关的封装了缴获到的消息结构的参数? 这个消息结构是怎样的?? 比如GetMessage钩子,lParam是Removal flag移除标志,wParam是个TMsg结构的指针? ?typedef struct tagMSG {???? // msg?? ??? HWND?? hwnd;?? ??? UINT?? message;? ??? WPARAM wParam;? ??? LPARAM lParam;? ??? DWORD? time;? ??? POINT? pt;? } MSG;? ?SDK中这么定义TMsg结构的? 等下会有例子源代码给大家看的,不要急? 继续... 三、实现 ? 下面我们来讲如何解决一、4.中提到的问题。? ?1.在自己的进程中访问其他进程的对象实例 ? 有了上面介绍的必备知识基础,那么现在这个问题对我们来说就不是很困难了,利用钩 ? 子由系统将DLL注入目标进程,这时DLL就在目标进程的地址空间中了,这样,DLL中的访问目标进程的对象实例的代码就可以工作了。? ?2.得到其他进程的DBGrid对象实例? ? DLL注入目标进程之后,实际上DLL和目标进程就在一个进程中了,那么按理说我们用 ? FindControl函数应该就可以由DBGrid句柄得到DBGrid对象实例的了,但实际并非如此! ? 实际写代码测试一下我们可以发现它返回的是nil。 我们来看看FindControl的源代码(Controls.pas中):? ?{ Find a TWinControl given a window handle } { The global atom table is trashed when the user logs off.? The extra test ? below protects UI interactive services after the user logs off. ? Added additional tests to enure that Handle is at least within the same ? process since otherwise a bogus result can occur due to problems with ? GlobalFindAtom in Windows.? } function FindControl(Handle: HWnd): TWinControl; var ? OwningProcess: DWORD; begin ? Result := nil; ? if (Handle <> 0) and (GetWindowThreadProcessID(Handle,OwningProcess) <> 0) and ???? (OwningProcess = GetCurrentProcessId) then // 判断调用进程ID是否为Handle所在进程 ? begin ??? if GlobalFindAtom(PChar(ControlAtomString)) = ControlAtom then ????? Result := Pointer(GetProp(Handle,MakeIntAtom(ControlAtom))) ??? else ????? Result := ObjectFromHWnd(Handle); ? end; end;? 安徽-小李(297099102) 14:23:55 function ObjectFromHWnd(Handle: HWnd): TWinControl; ? if (GetWindowThreadProcessID(Handle,sans-serif!important"> ???? (OwningProcess = GetCurrentProcessID) then ??? Result := Pointer(SendMessage(Handle,RM_GetObjectInstance,0)) ? else ??? Result := nil; 再看看其中使用到的ControlAtomString,ControlAtom,RM_GetObjectInstance的值是怎 样的(InitControls中):? 不是这个问题,我们的DLL已经注入目标进程了? 这是钩子帮我们完成的工作? ?procedure InitControls; ? UserHandle: HMODULE; ? WindowAtomString := Format('Delphi%.8X',[GetCurrentProcessID]); ? WindowAtom := GlobalAddAtom(PChar(WindowAtomString)); ? ControlAtomString := Format('ControlOfs%.8X%.8X',[HInstance,GetCurrentThreadID]); ? ControlAtom := GlobalAddAtom(PChar(ControlAtomString)); ? RM_GetObjectInstance := RegisterWindowMessage(PChar(ControlAtomString)); ? ... 不知道大家发现了没有? 看到这里,我们可以发现问题之所在了。ControlAtomString是根据模块句柄(模块加载基 地址)和线程ID动态生成的,目标进程的模块基地址就是EXE基地址,一般是0x00400000, 但DLL的模块加载基地址就不是这个了,默认是0x10000000,而实际上可能因为这个地址 已经被占用(有其他DLL被加载到这个地址)而进行重定位,所以初始化时添加的 ControlAtom和目标进程的ControlAtom的值就不一样,RM_GetObjectInstance也同样是不 一样的,那FindControl当然就不能找到DBGrid对象实例啦。? ?OK,清楚了这一点,解决起来就简单了,我们自己写个FindControl函数,以目标进程基 地址来动态生成ControlAtomString,添加ControlAtom就可以啦。 在DLL中取EXE的基地址,用GetModuleHandle(nil)即可。? 这个在DLL中做? ?var ? ControlAtom: TAtom; ? ControlAtomString: string; ? RM_GetObjectInstance: DWORD;? // registered window message ???? (OwningProcess = GetCurrentProcessId) then ????? Result := Pointer(SendMessage(Handle,0)); end;
initialization
finalization ? GlobalDeleteAtom(ControlAtom); ? ControlAtomString := ''; end. 嗯,上面的FindControl我是把Controls.pas中FindControl和ObjectFromHWND合成一个函数了? 这样就可以根据句柄获取到对象实例了! ? 根据句柄得到对象实例,还有另外一种方法,采用MakeObjectInstance的实现机制, ? 和InitWndProc的实现代码,根据句柄得到ObjectInstance指针,从而得到实例对象, ? 本文就不说了,有兴趣的网友请自行研究MakeObjectInstance、InitWndProc的代码。? ? 上面两个主要的问题解决了,大体上我们就解决了跨进程获取其他程序的DBGrid内容 ? 的问题了!? ?3.取DBGrid的内容? 句柄好找,FindWindow,FindWindowEx,关键是在于由句柄=>对象实例? 是的,是“绝对句柄”? ?// 获取目标进程中DBGrid的数据集的记录内容,保存到文件中 procedure ProcessDataSet(hCtrl: HWND); ? F: TextFile; ? FileName: string; ? Grid: TDBGrid; ? DataSet: TDataSet; ? I: Integer; ? Grid := TDBGrid(FindControl(hCtrl)); // 根据句柄取得对象实例 ? if (Grid <> nil) and (Grid.DataSource <> nil) and (Grid.DataSource.DataSet <> nil) then ??? FileName := ExtractFilePath(ParamStr(0)) + 'DataSet.txt'; // 目标程序运行目录下 ??? AssignFile(F,FileName); ??? if FileExists(FileName) then Append(F) else Rewrite(F); ??? try ????? DataSet := Grid.DataSource.DataSet;
????? Writeln(F,FormatDateTime('yyyy-MM-dd HH:mm:ss',Now),':');
????? DataSet.First; ????? while not DataSet.Eof do ????? begin ??????? for I := 0 to DataSet.FieldCount - 1 do ????????? Write(F,DataSet.Fields[I].AsString,','); ??????? Writeln(F); ??????? DataSet.Next; ????? end; ????? Writeln(F); ??? finally ????? CloseFile(F); ??? end; 大家觉得这个代码有没有问题???? 这个代码,我们在实际测试时可以发现还是存在问题的,运行会出错,问题就在于 Write(F,DataSet.Fields[I].DisplayText,');这一句,如果将这一句改成 ?null?? 是要取界面显示的内容? 呵呵,不是这个原因? ?value<>displaytext? ?TField.AsString的属性声明: ??? property AsString: string read GetAsString write SetAsString; ??? property DisplayText: string read GetDisplayText; ???? ??? function GetDisplayText: string; ??? procedure GetText(var Text: string; DisplayText: Boolean); virtual;? ?DisplayText属性声明? ?function TField.GetDisplayText: string; ? Result := ''; ? if Assigned(FOnGetText) then ??? FOnGetText(Self,Result,True) else ??? GetText(Result,True); end; procedure TField.GetText(var Text: string; DisplayText: Boolean); ? Text := GetAsString; ?TField类型的GetText的实现是直接调用GetAsString,我们看看特定类型的字段的 GetText函数的实现代码,如TIntegerField.GetText: procedure TIntegerField.GetText(var Text: string; DisplayText: Boolean); ? L: Longint; ? FmtStr: string; ? if GetValue(L) then ??? if DisplayText or (FEditFormat = '') then ????? FmtStr := FDisplayFormat else ????? FmtStr := FEditFormat; ??? if FmtStr = '' then Str(L,Text) else Text := FormatFloat(FmtStr,L); ? end else ??? Text := ''; 这个函数也并没有什么特别的代码,那到底是怎么导致这个错误的呢?这个就需要我们 再仔细地想想了,想当初这个问题困扰了好几天而不得解!还问过刘麻子大虾的,呵呵? 我们再来看看,用Field.GetText字符串类型会出错,而用Field.Value,Variant类型就 没有问题,那么应该是跟字符串类型有关了,再联想到这是在DLL中,传递字符串参数和 返回值时都要引用ShareMem作为工程的第一个单元,嗯,是不是跟字符串的自动内存管 理有关系?EXE和DLL是各自有自己的一套VCL类体系的,我们FindControl获取到的DBGrid实际上是EXE中创建的对象,而Field.GetText是个虚拟方法,它在VMT中占据一项,实际的函数地址存放在VMT中,尽管我们是在DLL中调用DisplayText属性和GetText函数的,但由于DBGrid对象实例是EXE中的,它的VMT中的实际的GetText函数是EXE中的,也就是说它是通过EXE的内存管理器,给返回字符串分配内存的,而我们的可爱的 那么我们也把ShareMem作为工程的第一个引用单元,编译生成DLL,测试,问题是否搞定了呢?没有!嘿嘿,尽管DLL我们uses ShareMem了,但目标进程并没有,而一般说来我们要用这种方法去跨进程获取其他程序的DBGrid内容,原因就在于我们没有目标程序的 源代码,也不能打开目标数据库或目标数据库的内容被加密了,那么我们当然是没法改 目标进程让它也uses ShareMem的,那就只有另谋出路的啦。解决问题的方法也是很简单, 字符串通过引用计数来进行内存管理的,当引用计数=1而这个字符串不再被使用时,引 用计数-1=0,这时就会释放字符串的内存的。嘿嘿,你不是在引用计数=0时才释放字符串 内存么,我就偏不让你释放,我用代码手工修改字符串的引用计数,让它比1大,那么 Delphi自然就会认为还有其他地方要使用这个字符串,就不会多此一举,滥做好人,帮 我们释放字符串的内容了。? 但是这是解决了出错的问题,却又衍生一个新的问题:内存泄漏,不过这个倒问题不大, 反正进程结束的时候,系统总是会释放这个进程所占的所有内存资源的,没有好的解决 办法,那目前我们也就只能这么着了,哈哈!? ? S: string; ??????? begin ????????? S := DataSet.Fields[I].AsString; ????????? if S <> '' then ??????????? Inc(PInteger(PChar(S)-8)^); // 增加字符串引用计数,避免Delphi自 ??????????????????????????????????????? // 动管理字符串内容的释放 ??????? end; 完整的源代码: http://lichengbin.iii-grp.com/Source/DBGrid.rar (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |