级别: 中级 李 冬瑞, 测试工程师, IBM
2008 年 8 月 11 日 由于 Robot 对界面控件识别的局限性,Robot 不能很好地操作一些控件,例如:日期时间控件 (DateTimePicker),属性页控件 (TabControl) 和工具栏控件 (Toolbar) 等等。本文讲述了一种在 Robot 中调用 Windows API 来操作这些常用 GUI 控件的方法,该方法可以应用于所有的 Windows 中常用的控件。使用该方法能极大地扩展 Robot 与 GUI 控件交互的功能,同时有助于提高 GUI 自动化测试脚本的稳定性和可移植性。
在 Rational Robot 中录制或者开发 GUI 自动化测试脚本的过程中,我们会发现 Rational Robot 在操作一些常用的 GUI 控件上有很多的局限性。这主要是因为 Rational Robot 无法识别被测控件的全部属性或者被测控件的属性会随着环境而改变导致其状态和录制脚本时候的状态不一致。
本文提供了一种方法可以对常用的 GUI 控件进行精确地操作。使用这个方法,能够保证我们对控件操作的正确性,同时还不依赖于控件的初始状态和环境,大大地提高了脚本的稳定性和可移植性。
在 Rational Robot 脚本中利用 Windows API 来操作 GUI 控件的基本原理
这个方法主要是利用了 Windows API 中的 SendMessage 函数能够模拟用户界面操作的功能。SendMessage 能够通过给特定的对象发送消息来达到和用户在界面上用鼠标或者键盘直接操作一样的功能。
下面给出这个方法的大致步骤:
-
初始化 SendMessage 需要用到的参数。
-
申请内存区域,通过给内存区域向 SendMessage 传递参数。
-
调用 SendMessage 函数发送操作的消息。
-
操作结束。
下面我们将通过使用 Windows API 来操作日期时间控件 (DateTimePicker) 为例子来介绍这个方法的具体实现。
在接下来的内容中,您将可以了解到如下的内容:
-
日期时间控件简介。
-
Rational Robot 对日期时间控件操作的局限性。
-
SendMessage 函数简介。
-
在 Rational Robot 中如何调用 Windows API 函数。
-
在 Rational Robot 中如何实现参数的地址传递。
-
利用 Windows API 对日期时间控件操作的方法。
-
利用 Windows API 来操作 GUI 控件的实现细节。
Windows 程序中常用的 GUI 控件
在 Windows 程序中常用的 GUI 控件有日期时间控件 (DateTimePicker),属性页控件 (TabControl) 和工具栏控件 (Toolbar) 等等。
日期时间控件 (DateTimePicker, 简称 DTP 控件 ) 通常和月历控件绑定在一起工作,这个控件允许用户选择日期、时间和月份或者置成空值。缺省时,用户可单击控件的右边的下拉按钮,即可弹出月历控件以供用户选择日期,通过风格的改变还可在日期时间控件内显示时间。
在月历控件中,用户可按击控件左右两边的箭头按钮 , 可前后翻页显示相关的月份。
图 1. 日期时间控件简单示例
在上图示例中,用户可以使用日期时间控件来设置 ExpireDate 属性的值。
Rational Robot 对日期时间控件操作的局限性
Rational Robot 在操作 GUI 控件上有很多的局限性。这主要是因为 Rational Robot 无法识别被测控件的全部属性或者被测控件的属性会随着环境而改变导致其状态和录制脚本时候的状态不一致。
例如,对于日期时间控件 (DateTimePicker) 来说,我们对它的操作可以是将其设置为空或者选择一个指定的日期。我们可以通过两种方法来指定日期控件中的日期值:一是在日期控件中直接输入日期;二是点击控件右边的下拉按钮并在弹出的月历控件中选择指定的日期。
但是 Rational Robot 对日期时间控件和月历控件的识别能力都有局限性,它只能记录用户在控件上鼠标操作的坐标值,而不能记录用户对其中的复选框的操作和实际选取的日期值。通常我们会根据当前日期控件中的日期格式,用 Robot 记录使用键盘输入的方式来设定日期值,但是这样的方法稳定性和可移植性比较差,当日期格式发生变化的时候,我们就必须修改输入日期的格式,否则回放的脚本就无法正确地设置日期值。
直接录制对日期时间控件的键盘操作的局限性
用键盘操作 ExpireDate 日期时间控件的步骤为:
-
用鼠标点击 Title 输入框,把焦点设置在 Title 输入框中。
-
按 Tab 键将焦点移到 ExpireDate 日期时间控件上。
-
按 Space 键选中复选框。
-
按右箭头键将输入焦点移动到月份,并输入 5。
-
按右箭头键将输入焦点移动到日期,并输入 14。
-
按右箭头键将输入焦点移动到年份,并输入 2008。
在 Robot 中录制上述操作得到如下的代码:
清单 1 对日期时间控件的键盘操作的代码
Window SetContext, "Caption=Modify Document", ""
EditBox Click, "Label=Title", ""‘点击 Title 输入框
InputKeys "{TAB}"‘按 Tab 键
InputKeys " "‘按 Space 键
InputKeys "{RIGHT}5"‘按右箭头键并输入月份 5
InputKeys "{RIGHT}14"‘按右箭头键并输入日期 14
InputKeys "{RIGHT}2008"‘按右箭头兼并输入年份 2008
|
以上的代码只适用于日期格式严格遵守 MM/dd/yyyy 的情况,当脚本被移植到别的操作系统,而该操作系统上的日期格式不是 MM/dd/yyyy 的时候(如图 2 所示,日期格式为 yyyy-MM-dd),以上的代码将无法正确地设置日期值。
图 2. 日期格式为 yyyy-MM-dd 时的日期时间控件
直接录制对日期时间控件的鼠标操作的局限性
直接在 Rational Robot 中录制将图 1 中的 ExpireDate 设置为 5/14/2008 的鼠标操作,我们得到如下的代码:
清单 2 对日期时间控件的鼠标操作的代码
Window SetContext, "Caption=Modify Document", ""
‘点击日期时间控件右边的下拉按钮
DateTime Click, "Label=ExpireDate", "Coords=461,10"
‘焦点设置在月历控件上
Window SetContext, "Class=SysMonthCal32", ""
‘在月历控件上选择 5/14/2008
Window Click, "", "Coords=88,80"
|
以上的代码只记录下了操作的控件以及鼠标在控件上点击的坐标。通常情况下,月历控件默认显示当前日期的月份表,当当前的日期不是五月而是其他月份的时候,月历控件将默认显示其他月份的月历。如图 2 所示,当前的日期是 6/12/2008,点击 ExpireDate 日期时间控件右边的下拉按钮,月历控件将显示六月份的月历,这时候清单 1 中的代码将无法保证设置的日期值仍然是 5/14/2008,而很有可能是 6/11/2008。
图 3. 月历控件显示六月份的月历
此外,在不同分辨率的机器上运行清单 1 中的代码时,很有可能会因为坐标的改变而无法点击到正确的按钮。
SendMessage 函数简介
我们知道 Windows 是一个消息驱动的操作系统。用户在 Windows 窗口中所做的操作会被转化成消息发送到相应的窗口进程,例如单击鼠标、改变窗口尺寸、按下键盘上的一个键都会使 Windows 发送一个消息给相应的应用程序窗口。窗口进程收到消息之后会根据消息的标识以及消息中附带的参数来处理消息。
Windows 提供了一个 Win32 API 函数 SendMessage, 该函数将指定的消息发送到一个或多个窗口,直到窗口程序处理完消息再返回。用户通过调用 SendMessage 函数发送消息给窗口可以实现和直接在窗口上操作相同的效果。
清单 3 SendMessage 的声明
Declare Function SendMessage Lib "user32" Alias "SendMessageA" (
ByVal hWnd As Long, ByVal wMsg As Long, ByVal wParam As Long, lParam As Any
) As Long |
其中的四个参数分别是:
- Hwnd32 位的窗口句柄,窗口可以是任何类型的屏幕对象,因为 Win32 能够维护大多数可视对象的句柄。
- wMsg消息的标识,通常是一个常量。这个常量可以是 Windows 单元中预定义的常量,也可以是自定义的常量。
- wParam通常是一个与消息有关的常量值,也可能是窗口或控件的句柄
- lParam通常是一个指向内存中数据的指针。
例如模拟键盘上的 TAB 键被按下, 就这样调用函数 :
SendMessage hWnd, WM_KEYDOWN, VK_TAB, 0 |
hWnd 是窗口句柄,WM_KEYDOWN 是键盘被按下的消息标识,收到此消息代表有按键被按下,VK_TAB 是被按下的键盘上的键子标识,这个表示是 TAB 键。
由此,我们可以得出一个设想,是否可以在 Rational Robot 中通过调用 SendMessage 函数发送相应的消息来模拟用户在界面上的操作而绕过 Rational Robot 在对 GUI 控件识别能力上的限制呢?
幸运的是,Rational Robot 支持在脚本中调用 Windows API 的函数。
在 Rational Robot 中如何调用 Windows API 函数
在 Rational Robot 的 GUI 脚本中可以直接调用 Windows API 函数,调用之前需要对该函数进行声明。例如:清单 4 显示了对 GetComputerName 函数的声明。
清单 4 声明 Windows API 函数示例
Declare Function GetComputerName Lib "kernel32" Alias "GetComputerNameA" (
ByVal lpBuffer As String, nSize As Long
) As Long |
在声明了函数之后,就可以像调用普通函数一样直接调用该 Windows API 函数了。清单 5 显示了调用 GetComputerName 函数的代码:
清单 5 调用 Windows API 函数示例
Const MAX_LENGTH = 128
Dim ComputerName As String*128
Dim RESULT As Long
RESULT = GetComputerName(ComputerName, MAX_LENGTH)
MsgBox Str(ComputerName) |
在 Rational Robot 中如何实现参数的地址传递
说到这里,我们还需要解决一个参数传递的问题。我们知道 Robot 的 GUI 脚本使用的是 SQABasic 语言,SQABasic 语言中没有指针的概念,但是大部分的 Windows API 函数接收的参数是结构化参数的指针或者地址。在 GUI 脚本中可以通过在被测程序的进程中申请共享内存区域,将结构化参数写入该内存区域,然后将该内存区域起始地址传递给 Windows API 函数来实现结构化参数的地址传递。然后 GUI 脚本可以通过写或者读该内存区域来实现参数的传入或者传出。
传入参数的具体步骤是:
-
根据当前控件的句柄获取该控件所在进程的唯一标识。
-
根据进程的标识打开一个该进程的句柄。
-
根据该进程的句柄申请在该进程内分配一块可读写的内存区域,并获取该内存区域的起始地址。该内存区域的大小和结构化参数的大小一致。
-
向该内存区域写入结构化参数的值。
-
将该内存区域的起始地址传递给 Windows API 函数。
下面我们还是以日期时间控件为例子,看看如何在 Rational Robot 中通过调用 SendMessage 函数来实现对日期时间控件的操作。
利用 Windows API 对日期时间控件操作的方法
下面列出日期时间控件相关的消息:
表 1. 常用的日期时间控件的消息|
消息标识
|
说明
| |
DTM_SETSYSTEMTIME
|
设置日期时间控件中的日期和时间值。
| |
DTM_GETSYSTEMTIME
|
获取日期时间控件中的日期和时间值。
| |
DTM_SETFORMAT
|
设置时间控件中的日期和时间的格式。
| |
DTM_SETRANGE
|
设置日期时间控件中允许的最大和最小日期时间值。
| |
DTM_GETRANGE
|
获取日期时间控件中允许的最大和最小日期时间值。
| |
DTM_SETMCCOLOR
|
设置月历控件中指定区域显示的字体颜色。
| |
DTM_GETMCCOLOR
|
获取月历控件中指定区域显示的字体颜色。
| |
DTM_SETMCFONT
|
设置月历控件中显示的字体。
| |
DTM_GETMCFONT
|
获取月历控件中显示的字体。
|
在 Windows 中,每个消息标识都对应一个数值,这些对应关系可以在 MSDN 上查到。但是这些消息标识在 Rational Robot 中并没有定义,所以在我们需要在脚本中预先对这些消息进行声明和初始化:
清单 6 声明和初始化 Windows 消息
Const DTM_FIRST= &H1000
Const DTM_GETSYSTEMTIME= (DTM_FIRST + 1)
Const DTM_SETSYSTEMTIME= (DTM_FIRST + 2)
Const DTM_GETRANGE= (DTM_FIRST + 3)
Const DTM_SETRANGE= (DTM_FIRST + 4)
Const DTM_SETFORMAT= (DTM_FIRST + 5)
Const DTM_SETMCCOLOR= (DTM_FIRST + 6)
Const DTM_GETMCCOLOR= (DTM_FIRST + 7)
Const DTM_SETMCFONT= (DTM_FIRST + 9)
Const DTM_GETMCFONT= (DTM_FIRST + 10)
|
接下来,我们就可以调用 SendMessage 发送 DTM_SETSYSTEMTIME 消息来模拟用户在窗口中设置日期的操作,调用的方法如清单 7 所示。
清单 7 发送 DTM_SETSYSTEMTIME 模拟用户操作
Result = SendMessage(
hWnd, '被操作的控件的句柄 ;
DTM_SETSYSTEMTIME, '消息的标识 ;
flag, '动作的标识 ;
lpSysTime, '结构化参数的起始内存地址 ;
); |
参数说明:
- flag:
- 动作标识,这个参数的取值必须是如下两个值之一:
表 2. DTM_SETSYSTEMTIME 消息动作标识的取值|
消息标识
|
说明
| |
GDT_VALID
|
根据 lpSysTime 所指的结构化参数设置日期时间控件。
| |
GDT_NONE
|
把日期时间控件的值设为空并且清空日期时间控件中的复选框。
|
- lpSysTime:
- 结构化参数的起始地址。这个参数是一个 SYSTEMTIME 结构,该结构包含了用来设置日期时间控件的日期和时间值。SYSTEMTIME 结构的定义如下:
清单 8 SYSTEMTIME 结构的定义
Type SYSTEMTIME
wYearAs Integer '年份
wMonthAs Integer '月份
wDayOfWeekAs Integer '星期
wDayAs Integer '日
wHourAs Integer '小时
wMinuteAs Integer '分钟
wSecondAs Integer '秒
wMilliSecondAs Integer '毫秒
End Type
|
通过 SendMessage 函数的返回值我们可以判断操作的成功与否。如果操作成功,则 SendMessage 返回一个非零值;如果操作失败,则返回零。
根据以上分析,我们很容易得到使用 SendMessage 发送 DTM_SETSYSTEMTIME 消息模拟用户操作日期时间控件的方法。以下是详细的步骤:
-
初始化 SYSTEMTIME 参数。
-
根据日期时间控件的识别方法获取该控件的句柄(调用 SQAGetProperty 获取控件的 hWnd 属性)。
-
根据控件句柄获取控件所在进程的唯一标识。
-
根据进程的标识打开一个该进程的句柄。
-
根据进程的句柄申请在该进程内分配一块可读写的内存区域,并获取该内存区域的起始地址。该内存区域的大小和结构化参数的大小一致。
-
向该内存区域写入 SYSTEMTIME 参数的值。
-
调用 SendMessage 函数发送 DTM_SETSYSTEMTIME 消息,并将该内存区域的起始地址传递给 SendMessage 函数。
-
释放申请的内存区域。
-
关闭该进程的句柄。
-
操作结束。
由于我们在 SYSTEMTIME 结构参数中明确地指定了日期和时间的值,所以操作成功完成后我们就能够确保控件被设置成了正确的日期值,而不受控件的日期时间显示格式或者初始状态的限制。
说到这里,我们可以得到一个在 Rational Robot 脚本中调用 Windows API 来操作 GUI 控件的方法。
利用 Windows API 来操作 GUI 控件的实现细节
这个方法分两个阶段来完成,第一阶段是准备阶段,这个阶段需要调查所需要模拟的操作对应的 Windows 消息的标识,动作的标识以及结构参数的定义。也就是说要确定 SendMessage 函数中的后面三个参数。这些通常可以从 MSDN 上面查到。
第二阶段就是脚本开发阶段。这一阶段我们需要定义数据结构并且调用 SendMessage 以及其他相关函数对控件进行操作,操作的步骤为:
-
初始化结构参数。
-
根据控件的识别方法获取该控件的句柄。
-
根据控件句柄获取控件所在进程的唯一标识。
-
根据进程的标识打开一个该进程的句柄。
-
根据进程的句柄申请在该进程内分配一块可读写的内存区域,并获取该内存区域的起始地址。该内存区域的大小和结构化参数的大小一致。
-
向该内存区域写入结构参数的值。
-
调用 SendMessage 函数发送操作的消息,并将该内存区域的起始地址传递给 SendMessage 函数。
-
释放申请的内存区域。
-
关闭该进程的句柄。
-
操作结束。
依据这个方法我们编写了一些函数来帮助我们的团队在 GUI 脚本中操作日期时间控件,属性页控件和工具栏控件等,这使得对这些控件的操作变得非常简单,很大的提高了自动化脚本的稳定性和可移植性。
本方法在最新的 Rational Robot V7 中,在英文和中文测试环境下对日期时间控件,属性页控件,工具栏控件,树形控件和列表控件测试通过。
结论
本文展示了如何使用 Windows API 函数模拟用户操作 GUI 控件,并给出了详细的解释和步骤,该方法还能扩展到对控件的其他操作上,例如通过发送 Windows 消息获取控件的状态和属性值等等。希望本文能够帮助有同样需求的团队扩展对此类控件的操作,提高他们的自动化测试脚本的稳定性和可移植性。
免责声明
本文包含解决方案。IBM 授予您(“被许可方”)使用这个解决方案的非专有的、版权免费的许可证。然而,解决方案是以“按现状”的基础提供的,不附有任何形式的(不论是明示的,还是默示的)保证,包括对适销性、适用于某特定用途或非侵权性的默示保证。IBM 及其许可方不对被许可方使用该软件所导致的任何损失负责。任何情况下,无论损失是如何发生的,也不管责任条款怎样,IBM 或其许可方都不对由使用该软件或不能使用该软件所引起的收入的减少、利润的损失或数据的丢失,或者直接的、间接的、特殊的、由此产生的、附带的损失或惩罚性的损失赔偿负责,即使 IBM 已经被明确告知此类损害的可能性,也是如此。
附录 1 函数源代码
清单 9 函数源代码
'###################################################################
'#
'# Function SetSystemTime(recMethod As String,
'# SystemTimeData As SYSTEMTIME)
'# As Integer
'#
'# 描述 :
'# 设置日期时间控件(DTP)的日期时间值 .
'#
'# SystemTimeData.wYear > 0: 按照 SystemTimeData 中的值设置控件的值。
'# SystemTimeData.wYear <= 0: 清空控件中的复选框。
'#
'# 参数 :
'# [ 输入 ] recMethod被操作的日期时间控件的识别方法。
'# [ 输入 ] SystemTimeData要设置的日期时间参数。
'#
'# 返回值 :
'# 如果成功则返回非零值;如果失败则返回零。
'#
'###################################################################
Function SetDTPSystemTime(recMethod As String, SystemTimeData As SYSTEMTIME) As Integer
Dim ResultAs Long' 操作结果返回值
Dim hWndAs Long' 控件的句柄
Dim ProcessIDAs Long' 控件所在进程的标识
Dim pHandleAs Long' 控件所在进程的句柄
Dim pMyItemMemoryAs Long' 内存区域的起始地址
' 根据控件识别方法获取其句柄
Result = SQAGetProperty(recMethod, "hWnd", hWnd)
' 根据控件句柄获取其所在进程的标识
Result = GetWindowThreadProcessId(hWnd, ProcessID)
If ProcessID <= 0 Then
SQALogMessage SQAWarning, _
"SetDTPSystemTime(): 无法获取进程标识。", _
" 控件识别方法是 : " & recMethod & "。"
Exit Function
End If
'**************************************************
' 打开该进程的一个句柄
'**************************************************
pHandle = OpenProcess(PROCESS_ALL_ACCESS, False, ProcessID)
'**************************************************
' 申请在该进程中分配一块内存
'**************************************************
pMyItemMemory = VirtualAllocEx(pHandle, 0, Len(SystemTimeData),
MEM_COMMIT, PAGE_READWRITE)
'**************************************************
' 将结构参数写入该内存中
'**************************************************
Result = WriteProcessMemory(pHandle, pMyItemMemory, SystemTimeData,
Len(SystemTimeData), 0)
'**************************************************
' 调用 SendMessage 发送消息设置控件的值
'**************************************************
If SystemTimeData.wYear > 0 Then
Result = SendMessage(hwnd, DTM_SETSYSTEMTIME, ByVal GDT_VALID, ByVal pMyItemMemory)
Else
Result = SendMessage(hwnd, DTM_SETSYSTEMTIME, ByVal GDT_NONE, ByVal pMyItemMemory)
End If
'**************************************************
' 释放内存并且关闭进程的句柄
'**************************************************
Result = VirtualFreeEx(pHandle, pMyItemMemory, 0, MEM_RELEASE)
Result = CloseHandle(pHandle)
'**************************************************
' 返回操作结果
'**************************************************
SetDTPSystemTime = Result
End Function
|

 |

|
附录 2 函数调用示例
这里我们将展示两个调用附录 1 中的 SetDTPSystemTime 函数的例子。
示例 1 在日期时间控件上设置日期值
在这个例子中,我们将图 1 中的 ExpireDate 控件的日期设置为 08/01/2008:
清单 10 将 ExpireDate 的日期设置为 08/01/2008
Dim utDate As SYSTEMTIME
utDate.wYear = 2008
utDate.wMonth = 8
utDate.wDay = 1
Call SetDTPSystemTime("Type=DateTime;Label = ExpireDate", utDate)
|
上述代码在对图 2 中显示的时间格式同样适用。
示例 2 将日期时间控件设为空
在这个例子中,我们将图 1 中的 ExpireDate 控件设为空并清空它的复选框:
清单 11 将 ExpireDate 的日期设置为空
Dim utDate As SYSTEMTIME
utDate.wYear = 0
Call SetDTPSystemTime("Type=DateTime;Label = ExpireDate", utDate)
|
执行上述代码后的效果如下图所示:
图 4. 将 ExpireDate 设置为空
参考资料 学习
获得产品和技术
讨论
关于作者  | |  | 李冬瑞,现为 IBM CDL 负责 DB2 Document Manager 功能测试的工程师。2004 年毕业于北京邮电大学计算机系,获得硕士学位,有 4 年 C++ 应用程序的开发和测试经验。现在从事软件自动化测试项目的开发工作。 |
对本文的评价
|