背景:SAP session的左下角消息非常有用,我们在做SAP的自动化脚本时可以设法读到这个消息的内容,作为程序后续动作的判断条件。如下图:
比如小爬之前给财务的同事制作了一个批量导出SAP各类报表的脚本工具:基于公司IT团队用ABAP编写的这几张表,SAP每次执行完导表动作,数据传输过程,左下角消息为“Transferring package1 of 1...”,当表格数据完整导出后,则显示“已传输N个字节”。我们的脚本可以去捕获“已传输...”这个消息,来判断报表内容是否已经完整导出,来决定是否要导出下一张报表。
事实上,通过原生的“脚本录制与回放”,我们可以得到这个消息:语法为:sapMessage=session.findById("wnd[0]/sbar").text。非常简单实用!实际编写脚本过程中遇到的问题是,当我们的脚本动态地顺序往下执行到等待报表出来的过程,控制权交到了导出的excel文件,我们的sapMessage=session.findById("wnd[0]/sbar").text无法获得执行,如果该消息文本恰好作为脚本中循环退出的条件,则脚本程序因为循环无法退出,导致界面卡死。
我想到的解决方法是,前期的参数输入等都通过脚本录制功能生成代码,到了点击“执行(F8)”这个动作,改由window api sendMessage控制,那么后续的控制权都在VB脚本下,我们再通过window api来捕获sap的窗口消息,而非sap原生提供的“session.findById("wnd[0]/sbar").text”方法。
我们需要捕获 sap session的窗口句柄,然后对其发送快捷键“F8”即可。在VBA中可以这样写:
Public Declare Function FindWindowEx Lib "user32" Alias "FindWindowExA" (ByVal Hwnd1 As Long, ByVal hWnd2 As Long, ByVal lpsz1 As String, ByVal lpsz2 As String) As Long Public Declare Function SendMessage Lib "user32" Alias "SendMessageA" (ByVal hwnd As Long, ByVal wMsg As Long, ByVal wParam As Long, lParam As Any) As Long Public Const WM_KEYUP = &H101 '释放 Public Const WM_KEYDOWN = &H100 '按下 Public Const VK_F8 = &H77 button_F8 = FindWindowEx(0, 0, "SAP_FRONTEND_SESSION", vbNullString) ‘类名通过spy++捕获 SendMessage button_F8, WM_KEYDOWN, VK_F8, 0 SendMessage button_F8, WM_KEYUP, VK_F8, 0
这段代码执行完后,程序就相当于给SAP发送了”F8“快捷键,执行导表。考虑到网络、数据库压力等原因,每次导表需要的时间并不确定,我们需要拿到报表已经完整导出的标记,然后关闭SAP自动打开的报表(如果每次SAP自动打开的excel报表不关闭,一次导出的报表数量过多时,电脑和程序都有可能卡死),方能执行下一次导表动作。
现在重点来了,如何捕获SAP左下角的消息”已传输N个字节“来作为程序判断的条件。
小爬一开始使用spy++来捕获该消息句柄,如图:
可惜每次执行SAP程序,该类名是动态变化的,所以基于类名来Findwindow显然定位不到它。
小爬接着观察spy++:
它总是出现”SAP 轻松访问“这个主session窗口句柄的第7个子窗口,于是我们可以循环使用7次FindwindowEX,就能找到该消息窗口的句柄msgHwnd,如:
Public Declare Function FindWindowEx Lib "user32" Alias "FindWindowExA" (ByVal Hwnd1 As Long, ByVal hWnd2 As Long, ByVal lpsz1 As String, ByVal lpsz2 As String) As Long Public Function mGetWindow(ByVal cName As String, ByRef fText As String) As Long Dim myStr As String * 100 Dim strLen As String Dim bWnd As Long Dim bWndback As Long Dim a As Integer strLen = Len(myStr) mGetWindow = 0 Do '获取子窗口标志 bWnd = FindWindowEx(0, bWndback, cName, vbNullString) 'List1.AddItem bWnd If bWnd <> 0 Then '获取子窗口标题 GetWindowText bWnd, myStr, strLen myStr = Trim(myStr) If InStrRev(myStr, fText) Then mGetWindow = bWnd Exit Do Else Sleep 200 End If End If bWndback = bWnd Loop End Function sessionHwnd = mGetWindow("SAP_FRONTEND_SESSION", "SAP 轻松访问") '得到左下角消息文本对应句柄,该句柄在SAP登录后不会变化 EnumChildWind = -1 Do hwdChild = FindWindowEx(sessionHwnd, hwdChild, vbNullString, vbNullString) '水平滚动条的句柄 EnumChildWind = EnumChildWind + 1 Loop Until EnumChildWind = 6 'sap左下角消息控件位于主界面下的child索引号总是为6(从0开始) msgHwnd = hwdChild
小爬给同事用的脚本工具稳定地管了一年多。上个月财务月结,却陆续有同事告诉我工具没法使用,动不大就卡死了。可这一年时间,公司的SAP版本(74.0)还有excel版本(excel2010)以及系统版本(win7 X64)都没有发生变化。小爬思前想后,未果!
近期,小爬捋了捋思路,仔细比对,终于发现其中端倪。原来是时间久了,很多同事用腻了原生的sap 主题和配色、字体等,然后选择了其他主题。您可能问了,主题变了,也会影响页面元素?整个捕获的逻辑不都是基于id、索引这些相对固定的东西定位的吗?
还真就巧了,经过SPY++探测窗口句柄发现,这个消息句柄在改动主题后的sap内,是位于sap session的第6个子窗口,而非先前的第7个。
小爬本想编写严格的操作手册,让用户改回默认的SAP主题和字体,去解决此问题。但是考虑到每个用户审美不同,且长时间只用一个主题确实会审美疲劳进而影响工作效率。小爬决定还是克难攻坚,使该脚本能兼容不同的SAP主题。
仔细观察SPY++,知道“SAP 轻松访问”的窗口句柄后,定位到“Afx Wnd110”类名的窗口,顺序FindwindowEx,当类名不再是“Afx Wnd110"后的第二个子窗口,就是我们要找的”sap左下角消息窗口“,这个逻辑在不同SAP主题下恒成立。代码层面该如何实现呢?
想要基于类名判断,那如何得到窗口的类名呢?这就需要涉及GetClassName 这个api函数了:
Public Declare Function GetClassName Lib "user32" Alias "GetClassNameA" (ByVal hwnd As Long, ByVal lpClassName As String, ByVal nMaxCount As Integer) As Integer Public Declare Function GetWindow Lib "user32" (ByVal hwnd As Long, ByVal wCmd As Long) As Long Public Const GW_HWNDNEXT = &H2 '寻找下一个具有相同关系的窗口,或寻找下一个具有相同关系的顶级窗口 Public sessionHwnd As Long, hwdChild As Long, msgHwnd As Long sessionHwnd = mGetWindow("SAP_FRONTEND_SESSION", "SAP 轻松访问") '得到左下角消息文本对应句柄,该句柄在SAP登录后不会变化 hwdChild = FindWindowEx(sessionHwnd, hwdChild, vbNullString, vbNullString) '得到第一个子窗口句柄 Do hwdChild = FindWindowEx(sessionHwnd, hwdChild, vbNullString, vbNullString) '水平滚动条的句柄 '获得实际的类名字符长度 iLEN = GetClassName(hwdChild, sCN, 256) '提取实际的类名 sCN = Left(sCN, iLEN) Loop Until sCN <> "AfxWnd110" '从上往下,最后一个类名为AfxWnd110的窗口往下两个窗口就是我们要找的sap消息窗口 msgHwnd = GetWindow(hwdChild, GW_HWNDNEXT) '得到sap消息窗口的句柄,全局变量
上面的代码中,GetWindow函数第二个参数GW_HWNDNEXT 代表:寻找下一个具有相同关系的窗口,或寻找下一个具有相同关系的顶级窗口。
通过上面的代码,我们终于得到了SAP的左下角消息句柄,紧接着,我们的程序在导表过程中等待时长就可以这样判断:
Do While InStr(msg01, "宸") = 0 SendMessage msgHwnd, WM_GETTEXT, 128, ByVal msg01 Sleep 500 Loop
之所以用Instr函数,是因为”已传输N个字节“的N值每次不同,需要模糊匹配字符串。脚本每隔500毫秒就取出msgHwnd的文本,与预设的”已传输..."作模糊匹配,而代码中的"宸"则是因为win7下sap的字符集解码问题,我们可以不用解决字符集的解码,只需在脚本的DO While判断条件中将错就错,即可。
经过一番测试和验证,终于解决了该自动化脚本针对SAP不同主题的兼容性问题,大功告成!无数次的经验告诉我们,只会用sap元素的脚本录制与回放功能,对于自动化工作是远远不够的,很多时候,我们要把它和windows底层的user32 api 函数结合来使用,才能解决大多数实际问题。上面的代码只是解决问题的其中一个思路,供借鉴,一起学习!