如何使用 UIAutomation 在 .NET 4.8 WinForms 应用程序中获取所有 ComboBox ListItem 值?

Posted

技术标签:

【中文标题】如何使用 UIAutomation 在 .NET 4.8 WinForms 应用程序中获取所有 ComboBox ListItem 值?【英文标题】:How to get all ComboBox ListItem values in a .NET 4.8 WinForms application with UIAutomation? 【发布时间】:2021-08-30 21:14:43 【问题描述】:

我希望这不是一个愚蠢的问题,我只是没有看到一个非常明显的解决方案,但是通过一些 GUI 测试,我的功能测试团队负责一个软件项目的工作,我遇到了一个变化ComboBoxes 的行为,同时通过我们使用 UIAutomation 与 GUI 元素交互的方式更新我们的 GUI 自动化库的行为。我们的软件产品正在使用 .NET 4.8 的最新开发版本 WinForms 应用程序。通过这次更新,我注意到元素的行为发生了变化。

例如,对于菜单栏项,为了获取菜单选择的子元素,需要在任何子项可见之前展开该项。 Inspect 观察到相同的行为。通过对我们的 GUI 库进行轻微的代码修改,我们能够相当容易地克服这个障碍。我可以在一定程度上理解为什么微软在 .NET 4.8 上做出了这种改变。 搜索***窗口的所有后代可能会在测试时造成灾难性的后果。

但是,对于组合框,其行为似乎略有不同。使用组合框,未展开时有两个子项:

 -Combobox
   |-Text item
   |-button item

ComboBox 有一个ExpandCollapsePattern。直觉告诉我,这很像菜单栏项,如果我展开它然后尝试获取子项列表,甚至后代,我应该会在集合中看到超过 2 个元素。即使在对 ComboBox 的 ExpandCollapsePattern 执行 Expand() 并重新关注 ComboBox 之后有相当长的延迟(我已经读过某些子项有时需要这样做),我仍然只看到 2 个元素,即使我m 寻找后代。

当我查看使用 Inspect 展开的 ComboBox 时,我看到以下内容:

 -Combobox
   |-List item
   |   |-List item
   |   |-List item
   |   |-List item
   |   |-List item
   |   |-List item
   |   |-List item
   |-Text item
   |-button item

使用这种新行为处理组合框的更新方法目前如下所示:

public List<AutomationElement> GetComboBoxEntries(AutomationElement parentElement)

    List<AutomationElement> items = new List<AutomationElement>();

    try
    
        var expandCollapsePattern = (ExpandCollapsePattern)parentElement.GetCurrentPattern(ExpandCollapsePatternIdentifiers.Pattern);
        expandCollapsePattern.Expand();

        //Slight delay
        DelayFor(1000);

        //Set focus to the combobox
        parentElement.SetFocus();

        //perform a FindAll() searching all descendants of the parentElement
        var collection = FindAllListItems(parentElement);

        //Add every element to the list
        foreach (AutomationElement element in collection)
         items.Add(element); 
    
    catch (Exception e)
    
        Console.WriteLine("There was an error performing the operation.");
        Console.WriteLine("Error: " + e.Message);
        Console.WriteLine("Stack Trace: " + e.StackTrace);
    
    return items;

我认为的一个解决方案,虽然在 ComboBox 中存在大量项目时不实用,但我预见解决我们的问题的方法是使用键盘/单击输入来迭代 ComboBox 的每个成员并捕获当前值元素的图案。我们以前不得不在引擎盖下做一些肮脏的巫术,但我真的不喜欢那种代码。我更喜欢在可能的情况下使用 UIA 保持清洁。此外,这似乎很乏味。

在此 .NET 4.8 更改之前,我们能够获取子列表项元素,检查每个元素的值以验证没有任何无效作为 GUI 冒烟测试的一部分。现在,您似乎需要跳过很多圈才能获得最终结果。

我是否遗漏了一些显而易见的东西?我更愿意将任何解决方案保留在 UIA 的范围内。就像意识到菜单栏项如何改变行为一样,我相信 ComboBoxes 有一个解决方案。

我只是没有看到它。希望你们当中有一点 UIA 专业知识的人可以为我指明正确的方向。谢谢。

【问题讨论】:

是的,托管 UI 自动化库在遍历树时存在一些问题。在面向 .Net Framework 4.8 的 WinForms 应用程序中,您可以找到 List 控件作为 RootElement 的子项(例如,var listElement = AutomationElement.RootElement.FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.ClassNameProperty, "ComboLBox"));)。如果你在[ExpandCollapsePattern].Expand() 之前和之后调用它,你应该做对了。 -- 否则,您可以使用 Win32 API (GetComboBoxInfo()),或者更改库或使用UIAComWrapper 顺便说一句,当您使用 Expand() 方法时,您不需要延迟(或者 DelayFor(1000) 是什么,我没有看到任何 async/await 实现,所以您是可能阻塞线程)。最后检查ExpandCollapseState.PartiallyExpanded 我没有代表将问题标记为已回答,但查看 RootElement 级别解决了我遇到的问题,即使 Inspect 似乎没有以这种方式显示树结构。我很好奇将 List 控件放在 .NET 4.8 中的 RootElement 级别的逻辑是什么。谢谢。 【参考方案1】:

这里有一些建议。

由于您正在处理 WinForms ComboBox,您不妨使用专用的 Win32 API 来获取列表控件的句柄(这里是 NativeWindow 派生对象,因此 UI 自动化看到它作为 Win32 控件,而不是 WinForm 控件)。 您可以使用GetComboBoxInfo() 函数,传递COMBOBOXINFO 结构和ComboBox 的句柄(由[AutomationElement].Current.NativeWindowHandle 返回)。

如果函数成功,则返回 NativeWindow 容器的句柄。 然后您可以使用 AutomationElement.FromHandle() 生成一个 AutomationElement。

这将允许在没有ExpandCollapsePattern 的情况下检索列表控件的列表项,因为您可以直接访问列表控件:

private AutomationElementCollection GetWinFormComboBoxListItems(AutomationElement comboBox)

    if (comboBox is null) return null;
    if (comboBox.Current.FrameworkId != "WinForm") 
        throw new ArgumentException("Not a WinForm Control");
    

    var cboInfo = new COMBOBOXINFO();
    cboInfo.Init();
        
    if (GetComboBoxInfo((IntPtr)comboBox.Current.NativeWindowHandle, ref cboInfo)) 
        var listElement = AutomationElement.FromHandle(cboInfo.hwndList);
        if (listElement != null) 
            var items = listElement.FindAll(TreeScope.Children, Automation.RawViewCondition);
            return items;
        
    
    return null;

Win32 declarations

[StructLayout(LayoutKind.Sequential)]
internal struct COMBOBOXINFO 
    public int cbSize;
    public RECT rcItem;
    public RECT rcButton;
    public ComboBoxButtonState buttonState;
    public IntPtr hwndCombo;
    public IntPtr hwndEdit;
    public IntPtr hwndList;
    public void Init() => this.cbSize = Marshal.SizeOf<COMBOBOXINFO>();


internal enum ComboBoxButtonState : int 
    STATE_SYSTEM_NONE = 0,
    STATE_SYSTEM_INVISIBLE = 0x00008000,
    STATE_SYSTEM_PRESSED = 0x00000008


[DllImport("user32.dll", CharSet = CharSet.Auto)]
internal static extern bool GetComboBoxInfo(IntPtr hWnd, ref COMBOBOXINFO pcbi);

如果你想一路走 UI 自动化,你可以尝试第一次从 ComboBox Element 中获取子 List Control。如果失败,请使用RootElement 作为父元素重试。这将在 NativeWindow 中检索列表控件 boxed 的 AutomationElement。

这将使用ExpandCollapsePatter 展开列表控件,使其对 UI 自动化可见。 您还可以使用Automation.AddAutomationPropertyChangedEventHandler() 创建一个自动化事件处理程序,传递一个用ExpandCollapsePattern.ExpandCollapseStateProperty 初始化的处理程序。 如果您想知道用户何时打开或关闭 ComboBox 的 DropDownList,或者选择了什么。

private AutomationElementCollection GetComboBoxListItems(AutomationElement comboBox)

    if (comboBox is null) return null;
    AutomationElementCollection items = null;
    bool wasCollapsed = false;

    if (comboBox.TryGetCurrentPattern(ExpandCollapsePattern.Pattern, out object exp)) 
        var expPattern = exp as ExpandCollapsePattern;

        var state = expPattern.Current.ExpandCollapseState;
        if (state == ExpandCollapseState.PartiallyExpanded) 
            Thread.Sleep(50);
        
        if (state == ExpandCollapseState.Collapsed) 
            expPattern.Expand();
            wasCollapsed = true;
        

        var condition = new AndCondition(
            new PropertyCondition(AutomationElement.ClassNameProperty, "ComboLBox", PropertyConditionFlags.IgnoreCase),
            new PropertyCondition(AutomationElement.IsEnabledProperty, true),
            new PropertyCondition(AutomationElement.ProcessIdProperty, comboBox.Current.ProcessId));

        AutomationElement listElement = comboBox.FindFirst(TreeScope.Children, condition);
        if (listElement is null && comboBox.Current.FrameworkId == "WinForm") 
            listElement = AutomationElement.RootElement.FindFirst(TreeScope.Children, condition);
        

        if (listElement != null) 
            items = listElement.FindAll(TreeScope.Children, Automation.RawViewCondition);
        
        if (wasCollapsed) expPattern.Collapse();
    
    return items;

【讨论】:

以上是关于如何使用 UIAutomation 在 .NET 4.8 WinForms 应用程序中获取所有 ComboBox ListItem 值?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 UIAutomation 中使用节点模块

即使在应用退出后,如何使用 UIAutomation 工具继续测试 iOS 应用?

我如何在 XCODE 8、Swift 3 中使用 UIAutomation

如何在 UIAutomation 中从句子中取出特定的单词?

如何在 iPhone 模拟器中工作的 UIAutomation 中获取 captureScreenWithName?

如何使用 Xcode 机器人在模拟器上运行 UIAutomation