演练:操作Qt应用中的树——TreeView

背景

需要对标准的树状结构——文件系统进行操作,假设需要访问某个固定的路径,使用Qt提供示例应用——DirView.exe为作为自动化的目标应用。

DirView样例界面

目标

针对Qt应用中的树控件,也就是TreeView控件进行自动化。Qt实现树控件的方式更像是一个多列的列表控件(ListView)。这种Qt实现树控件的方式导致自动化这个控件需要一些技巧,自动化的目标是能够在DirView.exe应用中,自动的索引到目标路径下。

为了实现该目标,我们需要掌握对树控件的几个操作:

  • 选中树节点
    • 树节点已经识别并且已展开的情况(不灵活)
    • 已知树节点的TreePath(灵活)
  • 树节点的展开/折叠

在学习自动化前,我们先对Qt实现的树结构进行一些了解。

Qt中的树控件

Qt中的树,其实很像一张表:
表有多行多列,树也有多行多列;
表有多个表头,树也有多列属性;
而且最重要的一点,也是常常让人困惑的一点,树与表一样,结构是完全扁平的,各级树节点不像显示中那样拥有层级关系,而是处于同一层,只有缩进不同。并且和其它复合控件一样,只将可视区域内的控件暴露给Accessibility API,比如树节点还未展开,或者滚动到可视范围外。

当目标节点移出可视范围后无法找到

这样的特性会带来的部分困难,可以使用CukeTest提供的API来进行规避。CukeTest为TreeTreeItem两个控件类型提供了专门为Qt应用适配的方法,可以点击查看对象操作方法——树

展开与折叠状态下的树控件:
折叠状态的树控件

展开状态的树控件

可以看到无论树的折叠或者展开的层级并不会影响实际的模型树,所有层级仍然像列表控件一样属于同一级,并且也不会将这些信息提供给Accessibility API。这种情况导致我们很难去判断树控件中各个节点的关联信息:节点的展开/折叠状态、目标节点的父子节点信息等等。

CukeTest对这种情况的处理方式,是使用键盘方向键来完成树节点的展开与折叠。你或许没有注意过当选中树节点后,右方向键可以展开树节点,而左方向键可以折叠树节点,CukeTest就是通过类似的方法完成对树节点的展开与折叠操作。

树节点的位置信息——TreePath

在Qt的树提供如此匮乏的节点信息的前提下,该如何去定位树节点的位置呢?CukeTest通过传入树节点的TreePath信息来确定节点的位置。所谓TreePath是一串字符串数组,代表到达目标树节点之前需要展开的父节点,更多的信息可以查看TreePath类型介绍

实际操作

因为此次的自动化针对的应用是模拟文件树的Qt应用Dir Name,因此我们尝试将文件路径转换为TreePath并用自动的展开与选中相关节点。

选中目标树节点

选中目标树节点通常有两种方式,一种是与自动化其它控件类似的方式,即先识别再操作的方式,但这里更推荐另外一种方式。在此之前我们先了解一下第一种方式。

1. 通过click()方法选中目标节点

通过树节点TreeItemclick()方法实现点击操作,如下:

  1. 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,这里假设是

  1. let treepath = ["Windows (C:)", "Windows", "System32"];

那么点击”System32“这个树节点的脚本可以写作:

  1. await model.getTree("Tree").select(treepath);

运行的时候可以看到CukeTest会自动的展开路径上的所有节点,选中最后一个节点。

select()方法类似的还有一个expandTo()方法,下面一节中也会提到。

展开/折叠树节点

接着是使用展开与折叠树节点,通常展开的目的就是为了点击到目标的树节点,如果是这种情况,仍然是建议直接使用Tree控件上提供的select()方法,但CukeTest仍然提供了与展开折叠相关的完整方法:

  • Tree树控件
    • expandTo()
    • collapseAll()
  • TreeItem树节点控件
    • expand()
    • collapse()

1. 通过expand()collapse()展开折叠树节点

与点击树节点类似,TreeItem控件还提供了展开与折叠方法——expandcollapse的方法,在模型管理器中也可以调试这两个方法。与树控件的click()方法类似,当树节点在可视范围外时操作会失败。如下:

  1. await model.getTreeItem("D:").expand();
  2. await Util.delay(2000);
  3. await model.getTreeItem("D:").collapse();

2. 通过树控件的expandTo()collapseAll()展开折叠节点

Tree控件上的select()方法选中节点相对的,也有用于展开与折叠的expandTo()collapseAll()方法,参数同样也是传入一个数组TreePath。仍然假设TreePath如下:

  1. let treepath = ["Windows (C:)", "Windows", "System32"];

那么展开与折叠目标节点的脚本可以写作如下:

  1. await model.getTree("Tree").expandTo(treepath);
  2. await Util.delay(2000);
  3. await model.getTree("Tree").collapseAll(treepath);

实现目标

在熟悉了以上操作,就可以将其整合为剧本文件和脚本文件,实现目标,也就是选中指定路径下的文件,这里目标文件假设为当前项目中step_definition文件夹中的definitions1.js文件。

剧本文件

剧本文件如下,前面两个场景都是描述上面的操作,最后一个场景定义的是实现目标的步骤,所以仅介绍最后一个场景中的两个步骤,其它的可以前往CukeTest Demos项目中查看。

  1. # language: zh-CN
  2. 功能: QtTree自动化
  3. 针对Qt中的TreeView控件进行自动化
  4. 场景: 操作树节点对象(需要识别模型)
  5. 假如点击模型中的树节点"D:"
  6. 假如展开和折叠模型中的树节点"D:"
  7. 场景: 操作树对象(不需要识别模型)
  8. 假如点击树中的'["Windows (C:)", "Windows", "System32"]'
  9. 假如折叠与展开树中的'["Windows (C:)", "Windows", "System32"]'
  10. 场景: 访问目标路径
  11. 假如访问并选中".\step_definitions\definitions1.js"文件
  12. 那么"definitions1.js"节点选中

编写脚本

首先是第一个步骤——访问并选中".\step_definitions\definitions1.js"文件:
在脚本文件头部加入库引用pathassert:

  1. const path = require('path');
  2. const assert = require('assert');

编写脚本如下,只要将传入的节点切分为TreePath传入到Treeselect()方法中即可。

如果传入的是相对路径,还需要拼接为绝对路径字符串后再切分。

  1. Given("访问并选中{string}文件", async function (relativePath) {
  2. let treepath = path.resolve(__dirname, '..', relativePath).split('\\');
  3. let tree = model.getTree('Tree');
  4. let foundFlag = false;
  5. // 由于磁盘名称不同这里为路径中的磁盘名做修改
  6. treepath[0] = treepath[0] == 'C:' ? 'Windows (C:)' : treepath[0];
  7. this.item = await tree.select(treepath);
  8. this.treepath = treepath;
  9. });

脚本最后两个this的调用目的是在场景中传递变量,对CukeTest中传递变量的方式不了解的可以点击场景中的变量传递了解。

在第一个步骤中就完成了选中的所有操作,第二个步骤中进行验证,验证目标节点是否被成功选中了,脚本如下:

  1. Then("{string}节点选中", async function (expectedItemName) {
  2. let itemName = await this.item.name();
  3. assert.strictEqual(itemName, expectedItemName);
  4. let selected = await this.item.focused();
  5. assert.strictEqual(selected, true);
  6. });

这就完成了全部的实现目标自动化的场景编写。接下来我们介绍生命周期(Hook)的写法,帮助我们执行自动化以外的配置操作,减少调试过程中重复操作,比如启动应用、登录、关闭应用等操作。

添加Hook

首先,常用的Hook可以在工具箱的Cucumber栏目中看到,这里我们拖拽BeforeAll这一Hook到脚本中,这一Hook会在任何操作运行前执行并且只会执行一次,比如运行项目、运行剧本、运行场景或者是运行步骤前。将工具箱中的BeforeAll拖拽到脚本中的空白位置生成模版并写入以下脚本:

  1. BeforeAll(async function () {
  2. Util.launchProcess("dirview.exe");
  3. })

通常配套的还会使用AfterAllHook,来执行关闭被测应用的操作,这里为了观察操作结果所以没有加,读者可以自己尝试编写。AfterAllBeforeAll刚好相反,是在所有操作都完成后才会运行。