微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

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

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

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

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

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

 -ComboBox
   |-Text item
   |-button item

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

当我查看使用 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 领域内保留任何解决方案。就像意识到菜单栏项的行为如何改变一样,我相信 ComboBox一个解决方案。

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

解决方法

这里有一些建议。

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

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

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

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 元素中获取子列表控件。如果失败,请使用 RootElement 作为父元素重试。这将在 NativeWindow 中检索装箱的列表控件的自动化元素。

这将使用 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;
}

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。