演练:Windows技术操作Qt应用中的树——TreeView
背景
需要对标准的树状结构——文件系统进行操作,假设需要访问某个固定的路径,使用Qt提供示例应用——DirView.exe为作为自动化的目标应用。
目标
针对Qt应用中的树控件,也就是TreeView
控件进行自动化。Qt实现树控件的方式更像是一个多列的列表控件(ListView)。这种Qt实现树控件的方式导致自动化这个控件需要一些技巧,自动化的目标是能够在DirView.exe
应用中,自动的索引到目标路径下。
为了实现该目标,我们需要掌握对树控件的几个操作:
- 选中树节点
- 树节点已经识别并且已展开的情况(不灵活)
- 已知树节点的
TreePath
(灵活)
- 树节点的展开/折叠
在学习自动化前,我们先对Qt实现的树结构进行一些了解。
Qt中的树控件
Qt中的树,其实很像一张表:
表有多行多列,树也有多行多列;
表有多个表头,树也有多列属性;
而且最重要的一点,也是常常让人困惑的一点,树与表一样,结构是完全扁平的,各级树节点不像显示中那样拥有层级关系,而是处于同一层,只有缩进不同。并且和其它复合控件一样,只将可视区域内的控件暴露给Accessibility API,比如树节点还未展开,或者滚动到可视范围外。
这样的特性会带来的部分困难,可以使用CukeTest提供的API来进行规避。CukeTest为Tree
与TreeItem
两个控件类型提供了专门为Qt应用适配的方法,可以点击查看对象操作方法——树。
展开与折叠状态下的树控件:
可以看到无论树的折叠或者展开的层级并不会影响实际的模型树,所有层级仍然像列表控件一样属于同一级,并且也不会将这些信息提供给Accessibility API。这种情况导致我们很难去判断树控件中各个节点的关联信息:节点的展开/折叠状态、目标节点的父子节点信息等等。
CukeTest对这种情况的处理方式,是使用键盘方向键来完成树节点的展开与折叠。你或许没有注意过当选中树节点后,右方向键→
可以展开树节点,而左方向键←
可以折叠树节点,CukeTest就是通过类似的方法完成对树节点的展开与折叠操作。
树节点的位置信息——TreePath
在Qt的树提供如此匮乏的节点信息的前提下,该如何去定位树节点的位置呢?CukeTest通过传入树节点的TreePath
信息来确定节点的位置。所谓TreePath
是一串字符串数组,代表到达目标树节点之前需要展开的父节点,更多的信息可以查看TreePath类型介绍。
实际操作
因为此次的自动化针对的应用是模拟文件树的Qt应用Dir Name
,因此我们尝试将文件路径转换为TreePath
并用自动的展开与选中相关节点。
选中目标树节点
选中目标树节点通常有两种方式,一种是与自动化其它控件类似的方式,即先识别再操作的方式,但这里更推荐另外一种方式。在此之前我们先了解一下第一种方式。
1. 通过click()方法选中目标节点
通过树节点TreeItem
的click()
方法实现点击操作,如下:
await model.getTreeItem(TreeItemName).click(0, 0, 1);
click(0,0,1)
与缺省调用click()
的效果一样,因为0,0,1
正是click
方法的缺省参数,代表”左键点击控件正中心“。
通过click
方法点击树节点会出现与点击列表项类似的隐患——当节点正中心没有在可视范围内,即控件只有不到一半的部分在可视范围内时,点击会失败。可以点击控件左上角来保证成功的点击,或者采取下面这种点击方法。
2. 通过树的select()方法选中目标节点
树控件Tree
提供了一个select(TreePath)
的方法,能够在已知目标节点的路径时直接选中目标节点,无论其处在展开还是折叠状态。读者应该要注意到,这里使用的是树控件,也就是树节点的父控件上的方法,这也是树控件与表格控件类似的地方,CukeTest推荐的自动化方式是在树控件上使用TreePath
来操作相关的树节点,而不是识别树节点之后再去进行相关的操作,这有利于完成树的自动化。
因此首先定义一个TreePath
,这里假设是
let treepath = ["Windows (C:)", "Windows", "System32"];
那么点击”System32“这个树节点的脚本可以写作:
await model.getTree("Tree").select(treepath);
运行的时候可以看到CukeTest会自动的展开路径上的所有节点,选中最后一个节点。
与
select()
方法类似的还有一个expandTo()
方法,下面一节中也会提到。
展开/折叠树节点
接着是使用展开与折叠树节点,通常展开的目的就是为了点击到目标的树节点,如果是这种情况,仍然是建议直接使用Tree
控件上提供的select()
方法,但CukeTest仍然提供了与展开折叠相关的完整方法:
Tree
树控件expandTo()
collapseAll()
TreeItem
树节点控件expand()
collapse()
1. 通过expand()
与collapse()
展开折叠树节点
与点击树节点类似,TreeItem
控件还提供了展开与折叠方法——expand
和collapse
的方法,在模型管理器中也可以调试这两个方法。与树控件的click()
方法类似,当树节点在可视范围外时操作会失败。如下:
await model.getTreeItem("D:").expand();
await Util.delay(2000);
await model.getTreeItem("D:").collapse();
2. 通过树控件的expandTo()
与collapseAll()
展开折叠节点
与Tree
控件上的select()
方法选中节点相对的,也有用于展开与折叠的expandTo()
与collapseAll()
方法,参数同样也是传入一个数组TreePath
。仍然假设TreePath
如下:
let treepath = ["Windows (C:)", "Windows", "System32"];
那么展开与折叠目标节点的脚本可以写作如下:
await model.getTree("Tree").expandTo(treepath);
await Util.delay(2000);
await model.getTree("Tree").collapseAll(treepath);
实现目标
在熟悉了以上操作,就可以将其整合为剧本文件和脚本文件,实现目标,也就是选中指定路径下的文件,这里目标文件假设为当前项目中step_definition文件夹中的definitions1.js
文件。
剧本文件
剧本文件如下,前面两个场景都是描述上面的操作,最后一个场景定义的是实现目标的步骤,所以仅介绍最后一个场景中的两个步骤,其它的可以前往CukeTest Demos项目中查看。
# language: zh-CN
功能: QtTree自动化
针对Qt中的TreeView控件进行自动化
场景: 操作树节点对象(需要识别模型)
假如点击模型中的树节点"D:"
假如展开和折叠模型中的树节点"D:"
场景: 操作树对象(不需要识别模型)
假如点击树中的'["Windows (C:)", "Windows", "System32"]'
假如折叠与展开树中的'["Windows (C:)", "Windows", "System32"]'
场景: 访问目标路径
假如访问并选中".\step_definitions\definitions1.js"文件
那么"definitions1.js"节点选中
编写脚本
首先是第一个步骤——访问并选中".\step_definitions\definitions1.js"文件
:
在脚本文件头部加入库引用path
与assert
:
const path = require('path');
const assert = require('assert');
编写脚本如下,只要将传入的节点切分为TreePath
传入到Tree
的select()
方法中即可。
如果传入的是相对路径,还需要拼接为绝对路径字符串后再切分。
Given("访问并选中{string}文件", async function (relativePath) {
let treepath = path.resolve(__dirname, '..', relativePath).split('\\');
let tree = model.getTree('Tree');
let foundFlag = false;
// 由于磁盘名称不同这里为路径中的磁盘名做修改
treepath[0] = treepath[0] == 'C:' ? 'Windows (C:)' : treepath[0];
this.item = await tree.select(treepath);
this.treepath = treepath;
});
脚本最后两个
this
的调用目的是在场景中传递变量,对CukeTest中传递变量的方式不了解的可以点击场景中的变量传递了解。
在第一个步骤中就完成了选中的所有操作,第二个步骤中进行验证,验证目标节点是否被成功选中了,脚本如下:
Then("{string}节点选中", async function (expectedItemName) {
let itemName = await this.item.name();
assert.strictEqual(itemName, expectedItemName);
let selected = await this.item.focused();
assert.strictEqual(selected, true);
});
这就完成了全部的实现目标自动化的场景编写。接下来我们介绍生命周期(Hook
)的写法,帮助我们执行自动化以外的配置操作,减少调试过程中重复操作,比如启动应用、登录、关闭应用等操作。
添加Hook
首先,常用的Hook
可以在工具箱的Cucumber
栏目中看到,这里我们拖拽BeforeAll
这一Hook
到脚本中,这一Hook
会在任何操作运行前执行并且只会执行一次,比如运行项目、运行剧本、运行场景或者是运行步骤前。将工具箱中的BeforeAll
拖拽到脚本中的空白位置生成模版并写入以下脚本:
BeforeAll(async function () {
Util.launchProcess("dirview.exe");
})
通常配套的还会使用
AfterAll
的Hook
,来执行关闭被测应用的操作,这里为了观察操作结果所以没有加,读者可以自己尝试编写。AfterAll
与BeforeAll
刚好相反,是在所有操作都完成后才会运行。