获取windows程序界面数据

项目实战 同时被 2 个专栏收录
33 篇文章 1 订阅

结论

不是所有程序都可以通过系统来获取其句柄,进而拿到程序进程的数据,跨进程的通信其实很复杂,win32提供的api可以访问的窗口组件,以及可以获取的窗口组件属性有限,其实很多时候不能满足全部需求


可以尝试的其他方向


最后的挣扎

基础知识

第一次上手这种项目,大概知道是获取windows窗体的句柄,大概的方向是使用C++或者C#(虽然我这几年基本都全用python,C系列的忘得差不多,但是该捡起来还是要捡起来,哈哈哈)

win7/win10查看某个进程的句柄数

win7下,比较简单:

任务管理器->选择 进程tab->菜单栏的查看->选择列->句柄数复选框选择,然后在进程tab页面就可以看到多了一列 句柄数 的显示。

win10下,比win7隐藏的深一点点
任务管理器->选择 详细信息tab->点击 名称/状态/用户名 这一个表头 右击->选择列->选择 句柄数复选框 OK 就可以在 详细信息tab中看到某个程序的句柄数了(注意,一个程序的句柄数其实是会变的,和该程序当前窗口中控件的个数有关,详见下面句柄的概念)
在这里插入图片描述
此外,还发现win10的任务管理器其实更加人性化了,进程tab中可以右击名称这一表头,然后可以选择想看的东西,像PID,命令行,进程名称这种都可以看得到,就很好
在这里插入图片描述


句柄的概念

  • 数值上,是一个32位无符号整型值(32位系统下);
  • 逻辑上,相当于指针的指针;形象理解上,是Windows中各个对象的一个唯一的、固定不变的ID;
  • 作用上,Windows使用句柄来标识诸如窗口、位图、画笔等对象,并通过句柄找到这些对象。
  • 有一个固定的地址(存储句柄的32位无符号整型值(32位系统下)),指向一个固定的位置(区域A),而区域A中的值可以动态地变化,它时刻记录着当前时刻对象在内存中的地址。这样,无论对象的位置在内存中如何变化,只要我们掌握了句柄的值,就可以找到区域A,进而找到该对象。

还有些注意事项

  • 所谓“唯一”、“不变”是指在程序的一次运行中。如果本次运行完,关闭程序,再次启动程序运行,那么这次运行中,同一对象的句柄的值和上次运行时比较,一般是不一样的。
    其实这理解起来也很自然,所谓“一把归一把,这把是这把,那把是那把,两者不相干”(“把”是形象的说法,就像打牌一样,这里指程序的一次运行)。
  • 句柄是对象生成时系统指定的,属性是只读的,程序员不能修改句柄。
  • 不同的系统中,句柄的大小(字节数)是不同的,可以使用sizeof()来计算句柄的大小。
  • 通过句柄,程序员只能调用系统提供的服务(即API调用),不能像使用指针那样,做其它的事。

参考:

Windows MFC、SDK和API的区别和联系

在搜索相关代码(如:如何获取窗口的句柄时),经常看到这三种东西,所以搜了下,不喜欢太糊里糊涂的。

搜索过程中发现其实很多人都分不清概念,但是不影响使用和完成任务,虽然我也不求甚解,但是还是不喜欢那么那么一无所知,而且这种专业术语乱用也容易给后来者造成困扰吧

  • API:Application Programming Interface。Windows操作系统提供给应用程序编程的接口, 简称 为API函数。所有主要的Windows函数都在Windows.h头文件(这个文件好像需要安装一些开发包才有,暂时看不到在哪,有兴趣可以自己找找)中进行了声明。使用windows API创建的能在windows上运行的程序统称为windows程序。 Win32是一个子系统。Win32是Windows的一个子系统,还有另外的子系统如OS/2、POSIX、WOW等。不同的子系统系统提供了不同的编程接口,即API,一般说的API指的就是Win32 API.

    以 WOW为例(经常可以在系统中看到的这个东西):百度百科上的解释:WOW64 (Windows-on-Windows 64-bit)是一个Windows操作系统的子系统, 它为现有的 32 位应用程序提供了 32 位的模拟,可以使大多数 32 位应用程序在无需修改的情况下运行在 Windows 64 位版本上。
    在这里插入图片描述
    此外,Win32程序用得最多的还是Win32子系统的DLL们,最核心的DLL包括:kernel32.dll、User32.dll、Gdi32.dll、Advapi32.dll。这些DLL包装了ntdll.dll的native API。其中Gdi32.dll比较特殊,它与核心态的win32k.sys直接保持联系,以提高NT系统的图形处理能力。Win32子系统的DLL们提供的接口函数在MSDN文档中被详细介绍,它们就是Win32 API

  • MSDN(Microsoft Developer Network 微软开发者网络):官方-Microsoft Docs,大概界面如下。(由于API越来越多,为了更好帮助开发者,微软向开发人员提供了MSDN,其实一套帮助系统,包含大量的开发文档、技术文章和示例代码,对于初学者来说,学会使用MSDN并从中汲取知识,是必须要掌握的技能)
    在这里插入图片描述

  • Win32 SDK:SDK(Software Development Kit)中文是软件开发包。则Win32 SDK是Windows 32位平台下的软件开发包,包括了API函数、帮助文档、微软 提供的一些辅助开发工具。SDK实际上就是开发所需资源的一个集合。(能更方便我们的开发)

  • 提示:API和SDK是一种广泛使用的专业术语,并没有专指某一种特定的API和SDK,例如,语音卡API、语音卡SDK、Java API、Java SDK等。自己公开的DLL函数也可以叫API!!!

    • 一般来讲,狭义上的API指 MS公开的函数。比如MSDN中介绍的函数。
    • 广义的API可以包括所有的函数,你自己的函数也算,未公开的也是.世界上一切函数。都可以叫API–Application Programming Interface
  • MFC:Microsoft Foundation Classes微软基础类。用于在C++环境下编写应用程序的一个框架和引擎。(也可以说,MFC是Win API与C++的结合后的再一次封装)。MFC是MS对API的一个封装,也就是一个C++类库,当然MFC比一般类库庞大,所以有人称之为应用程序框架。但其本质还是一个类库

  • 提示: SDK是基于C语言的,而MFC是基于C++的,这是最根本的区别。MFC主要封装的是界面、文件、WinInet和线程等函数。MFC除了封装API,最重要的是它的体系结构,它所使用的Doc/View结构是SDK中没有的,这种架构是比较特殊的。MFC是微软的基本类库,对很多东西已经进行了封装,因此使用起来简单、方便。SDK是采用较一般的C语言,但很灵活。一般编写简单的程序,使用MFC应该能达到要求。但如果编写功能强大的程序,则使用SDK较多,尤其是底层的开发。

PS:根据网上搜索到的资料,windows操作系统的内核是c写的,外围的一些内容是c++写的,所以windows API应该是使用C语言和汇编语言写的。SDK + C 完全可以进行所有的windows程序开发,也可以采用MFC + C++,当然,你要用SDK + C++ 也是你的自由,但MFC + C不可能,因为MFC是C++写的,C不支持类,所以三者的关系如下图(自己随便画的,仅供示意)
简单说,API是接口,SDK是包含API声明的开发包,MFC是封装API的类库.
在这里插入图片描述

  • Win32项目:Microsoft Windows操作系统32位环境下由C/C++语言调用API函数编写的项目(不一定有窗体,单纯的一个后台应用)。
  • Windows窗体应用程序:用户计算机上运行的客户端应用程序,可显示信息、请 求用户输入以及通过网络与远程计算机进行通信。在开发大型应用程序时,可能需要研 究 .NET Framework 和它所提供的类。MFC和Windows窗体应用程序的结果完全一样,都是用面向对象的思想做界面。虽然MFC也可以开发出Windows窗体应用程序,但是Windows窗体应用程序使用更方便,灵活性不如MFC。
  • 提示: win32应用程序是指可以在32位或以上Windows系统中运行的程序,概念比windows窗体大,严格说来,窗体程序也是win32应用程序。Windows窗体应用程序必须在.net环境中应用,就是说你编译好的软件在没有.net framework的机器上是不能运行的,而且功能的实现也要受framework的限制,不能随心所欲。

参考:

Windows MFC(C++开发)

如果选择Windows API和SDK开发,就要用C语言,没有对象和类。。。甚是头疼,我觉得我不行,so我还是选择MFC进行开发这种方式来搞比较好
如果有想看SDK和API的,可以参考:

二者直观的代码区别及总结性比较(API/SDK vs MFC),参考


这里可以看到的一个概念是,单纯的句柄概念,其实只出现在Windows API/SDK编程中,MFC里其实使用的就是对象指针。(百度发现: MFC中有大量的句柄包装类。所谓句柄包装类,指的是这些类是封装了系统对象的句柄,并提供了一组成员函数作为访问系统对象的接口。)


目测MFC在网上的资料最多,也是使用最广泛的,除了参考链接之外,用的最多的应该还是MSDN上关于MFC的使用介绍,如果最开始搜到的是英文,把连接中的en-us改成zh-cn就好了(有时报错,没有这个页面/有概率是机器翻译,不是人编辑的)。

Spy++

这个工具很方便,可以用来查看程序的句柄,窗口,进程,线程等,微软Visual studio的文档说明中给的很清楚,
在这里插入图片描述
在这里插入图片描述

程序

这里考虑到之后编译后的exe程序要运行在不同系统类型的电脑上(32位/64位 win10/win7),所以经过同事大佬的建议,最好是使用C++/C# 在VScode编译中选择,得到不同系统类型的发布版本。
参考:

两个方向:python来写(我比较熟悉,但是编译成exe之后可能会出现问题,尤其涉及到句柄的使用),C++来写(最优选择,性能高,适配方便)

关于C++,如果也是像我一样四五年没再用过,建议去看我另一篇文章(待填坑)稍微上手一下。

使用MFC的方式获取界面信息

搜了一波,做应该也可以做,只是可能在后续遇到问题时,由于不明白更准确的术语描述,可能后续解决问题会有一定的障碍

参考:

Python实现

整合上述资料之后得到的样例代码demo:

import win32gui
import win32api
classname = "MozillaWindowClass"
titlename = "百度一下,你就知道 - Mozilla Firefox"
#获取句柄
hwnd = win32gui.FindWindow(classname, titlename)
# 很奇怪 官方文档这里  已经没有这个FindWindow了 全是后面有后缀的 FindWindowA  FindWindowExA等等
# 如果没有类名 可以改成 None

# 直接 print(hwnd) 会得到一串数字(以16进制形式打印 可以看到和spy++中显示的句柄号一致)
print('%#x' % hwnd )
# 如果窗口无标题栏或文本,或标题栏为空,或窗口或控制的句柄无效,则返回值为零。函数不能返回在其他应用程序中的编辑控件的文本
print(win32gui.GetWindowText(hwnd))
# 获取某个窗口的类名
print(win32gui.GetClassName(hwnd)) 
#获取窗口左上角和右下角坐标
left, top, right, bottom = win32gui.GetWindowRect(hwnd)

# 获取当前大窗体中某个已知类名 不知窗体名的组件/子窗体
child_class='AfxWnd40'
child_handle = win32gui.FindWindowEx(handle, None, child_class, None)
print('%#x' %child_handle)
# 但是FindWindowEx只会返回第一个满足条件的子窗体句柄

# 遍历输出当前父窗体下所有子窗体的句柄
child_class_list = []
win32gui.EnumChildWindows(handle, lambda handle,param:param.append(handle), child_class_list)
print(child_class_list)

# 使用GetClassName查看是否有符合条件的句柄
class_set=[]
for hwnd in child_class_list:
    hwnd_class=win32gui.GetClassName(hwnd)
    class_set.append(hwnd_class)
print(list(set(class_set)))

# 获得想要的子窗体句柄后,主要是MSFlexGridWndClass这个窗体,就可以对其进行操作

# 首先 要根据句柄获取这个对象 然后才可以对其进行调用

在这里插入图片描述
在这里插入图片描述

关于FindWindowEx()函数

参考:

FindWindow(
  lpClassName,        {窗口的类名}
  lpWindowName: PChar {窗口的标题}
): HWND;              {返回窗口的句柄; 失败返回 0}
//FindWindowEx 比 FindWindow 多出两个句柄参数:
FindWindowEx(
  Parent: HWND;     {要查找子窗口的父窗口句柄}
  Child: HWND;      {子窗口句柄}
  ClassName: PChar; {}
  WindowName: PChar {}
): HWND;
{
如果 Parent 是 0, 则函数以桌面窗口为父窗口, 查找桌面窗口的所有子窗口;
如果  是 HWND_MESSAGE, 函数仅查找所有消息窗口;
子窗口必须是 Parent 窗口的**直接子窗口**;
如果 Child 是 0, 查找从 Parent 的第一个子窗口开始;
如果 Parent 和 Child 同时是 0, 则函数查找所有的顶层窗口及消息窗口.
}
  • 很明显 这里有一个先决条件,就是子窗口必须是父窗口的直接子窗口(如果想要进行迭代,必须自己手动编写函数搞定)
  • Child HWND :子窗体句柄。查找从在Z序中的下一个子窗体開始。子窗体必须为hwndParent窗体的直接子窗体而非后代窗体。如果Child HWND为NULL,查找从hwndParent的第一个子窗体開始。如果hwndParent 和 Child HWND均为NULL,则函数查找全部的顶层窗体及消息窗体。
  • ClassName:代表要查找的类名的字符串,或字符串指针。如"“BUTTON”、 “LABEL”、 "TEXTBOX"等。假设该參数为一个成员,则它必须为前次调用theGlobaIAddAtom函数产生的全局成员。该成员为16位,必须位于ClassName的低16位,高位必须为0。
  • Window:代表要查找的窗体名(窗体标题)束字符串。假设该參数为 NULL,则为全部窗体全匹配。
  • 返回值:
    假设函数成功,返回值为具有指定类名和窗体名的一个窗体句柄。假设函数失败,返回值为NULL。(注意,这个只返回一个窗体句柄)
  • 如果要查找具有指定类名或窗体名的所有子窗体的句柄,可先将Child HWND参数的值设为NULL进行一次查找,再用前一次调用FindWindowEx所得的的返回值作为Child HWND参数的值持续调用此函数,直到所得返回值为0。

EnumChildWindows()函数

鉴于FindWindowEx()函数只能返回第一个满足条件的句柄,无法满足我的需求,so决定看看其他函数

BOOL EnumChildWindows(
  HWND        hWndParent,
  WNDENUMPROC lpEnumFunc,
  LPARAM      lParam
);
参数说明:
+ hWndParent Type: HWND
要进行遍历的子窗口的父窗口的句柄,如果父窗口的句柄hWndParent 为None,则这个函数就等价于EnumWindows()
+ lpEnumFunc 指向应用程序定义的回调函数的指针。 有关更多信息,请参见EnumChildProc。
+ lParam Type: LPARAM
应用程序定义的值,将传递给回调函数。
+ 如果子窗口创建了自己的子窗口,则EnumChildWindows也会枚举这些窗口。在枚举过程中以Z顺序移动或重新定位的子窗口将被正确枚举。 该函数不会枚举在枚举之前销毁的子窗口或在枚举过程中创建的子窗口。

关于 MSFlexGridWndClass

使用SendMessage从控件中获取信息
len = win32gui.SendMessage(form_handle, win32con.WM_GETTEXTLENGTH, 0, 0) + 1000  # 获取控件中内容的文本长度 这里有个问题
# SendMessage函数用于獲取不含截尾空位元組的文字長度的長度 
# 返回值:返回複製字元的數量,不包括截尾的空位元組(这里有一种 截尾的空位元素)
# 头文件:winuser.h;输入库:user32.lib;Unicode:在Windows NT环境下以Unicode和ANSI方式实现
buffer = win32gui.PyMakeBuffer(len)
win32gui.SendMessage(form_handle, win32con.WM_GETTEXT, len, buffer)
print(buffer)

address, length = win32gui.PyGetBufferAddressAndLen(buffer)
text = win32gui.PyGetString(address, length)
print('获取内容的长度', length)
# sys.stdout = io.TextIOWrapper(sys.stdout.buffer, errors = 'replace', line_buffering = True)
sys.stdout = io.TextIOWrapper(sys.stdout.buffer,encoding='utf-8')
print(text)

根据

上述路径不可行原因
考虑从控件所在的进程里抓取数据

搜索了一波,这个好像也很复杂,而且python好像无法直接

import psutil
import win32process
processId= win32process.GetWindowThreadProcessId(hwnd)
print(processId)
// 返回了一个列表 [8732, 9612]
Returns a tuple consisting of 2 ints:
Thread ID (tid) 线程id
Process ID (pid) 进程id(一般用这个)

//所以更正确的写法是
tid, pid = win32process.GetWindowThreadProcessId(hwnd)
active_window_path = psutil.Process(pid).exe()

窗体组件说明

还是要了解下常见的控件

  • afxwnd42 当你创建窗口时没有指定窗体类时…MFC默认帮你创建的类,没有子类化的就是AFXWND42,子类化了的就是AFXOLECONTRLWND42

窗体类型

使用Spy++来查看窗体的相关信息,以下都是(我所面对的程序的)窗口类名,肯定也有一些是 第三方自定义类

  • ThunderRT6FormDC 窗体类名
  • ThunderRT6UserControlDC
  • ThunderRT6Timer
  • ThunderRT6CommandButton 按钮类名:
  • ThunderRT6ComboBox
  • ThunderRT6CheckBox
  • ThunderRT6Frame
  • ThunderRT6TextBox
  • ThunderRT6OptionButton
  • ThunderRT6Data
  • MSFlexGridWndClass 感觉这个应该是我要重点关注的
  • StatusBar20WndClass
  • DBComboWndClass
  • ScrollBar
  • ThunderRT6ListBox
  • Edit
  • Button
  • Afxwnd40
    VB6 Grid Control with WndClass:AfxWnd40 not recognised
    ,大概可以猜到,我所要处理的那个程序其窗口中的内容是使用grid AfxWnd40进行组织的。(历史比较久远,比较老的程序,不好搞哦)
    在这里插入图片描述
    搜索之后了解到,上面那些组件,很多都是VB里的。。。

感谢ThunderForm,在得到进程的窗口句柄中,经常用到,不知道这个是什么?
ThunderRT6FormDC 是 VB6 窗体的 class name。这个是固定的,在调用API中有时用到。Thunder 是当年的一款 编程软件。 微软收购下来, 修改后改名为 Visual Basic 1.0

窗体消息

在这里插入图片描述
可以看到,最左边一栏就是当前监视的控件/窗口的句柄,右边有 WM_SETTEXT设置文本(设置文本后面一行还有 WM_SETEXT fSucceeded:Fasle、True 来表明状态是否改变), 可以看到有 WM_MOUSEMOVE 鼠标移动 WM_CAPTURECHANGED 捕获改变 都是一些预定义的动作(还好之前在C#编程的时候看过窗体。。。)
在这里插入图片描述
其他一些按钮改变同理

VB6.0文档参考

参考

  • 6
    点赞
  • 4
    评论
  • 16
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 技术黑板 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值