WPF UIAutomation:获取用户控件的子控件

9
我是NordVPN的用户,一直没有遇到任何问题。现在出于某些需求,我需要设置一些属性,如协议(复选框)和从其他应用程序中单击按钮。但该应用程序区域看起来像是自定义控件,而UIAutomation无法深入其中。该自定义控件内部的元素没有任何自动化ID。因此,我需要知道如何使用UIAutomation和White Framework遍历WPF应用程序中的用户控件,就像应用程序窗口的其他部分一样。到目前为止,我尝试过使用TreeWalker(无法读取所有元素),并尝试从其位置AutomationElement.FromPoint()获取元素,但它再次给出了整个自定义控件(根据其边界确定),我仍无法遍历它。请问如何从UIAutomation中深入自定义控件?

enter image description here

记录一下,Snoop 能够读取元素,但 VisualUIAVerify.exe 不能。

你使用过Windows SDK中的inspect.exe吗? - kurakura88
我不确定他们能够操纵实际控件的程度。我也觉得很奇怪,WPF 控件并没有暴露给 UI Automation。除非控件完全是自定义的,否则 WPF 默认会公开控件。 - o_weisman
这个应用程序似乎基于http://caliburnmicro.com/等,可能不支持UI自动化(它使用了很多本地技巧来实现那种外观)。 - Simon Mourier
@SimonMourier 我使用Caliburn Micro开发了应用程序,它并不会阻止我们查看控件。此外,除了这个部分之外,应用程序的其余部分都很容易自动化,比如在选项卡之间切换和搜索等。 - Manvinder
1
这可能有效 - 尝试像Snoop一样附加到外部应用程序(https://dev59.com/oGAg5IYBdhLWcg3wBXKs),并且当您拥有应用程序窗口时 - 遍历可视树。一旦找到需要自动化的控件 - 将x:Name复制到x:AutomationProperties.Name(或自动生成名称)。 - Maciek Świszczowski
显示剩余2条评论
1个回答

3

编辑1 - 澄清: 对于我来说,如果没有唯一标识来进行自动化操作,控件是不可见的。标识因素可以是名称、ID、同级或父级。


预期的是,缺少自动化ID导致控件在UI自动化树API中不可见。

为了解决这个问题,并且知道它们在Snoop应用程序中是可见的-您可以使用底层逻辑(Snoop使用的逻辑)来编程自动化这些控件。

步骤

  1. Download the binaries for SnoopUI, and add them to your project. Make sure to keep the compile option as 'None' and are copied to output directory.

    enter image description here

  2. Next step would be add a helper method, that uses these binaries to inject your dll with automation logic into target application (which is NordVPN) in this case. Once the dll is injected into target process, the ManagedInjector also invokes the method that is sent as parameter.

    public class Helper
    {
        public static void Inject(IntPtr windowHandle, Assembly assembly, string className, string methodName)
        {
            var location = Assembly.GetEntryAssembly().Location;
            var directory = Path.GetDirectoryName(location);
            var file = Path.Combine(directory, "HelperDlls", "ManagedInjectorLauncher" + "64-4.0" + ".exe");
    
            Debug.WriteLine(file + " " + windowHandle + " \"" + assembly.Location + "\" \"" + className + "\" \"" + methodName + "\"");
            Process.Start(file, windowHandle + " \"" + assembly.Location + "\" \"" + className + "\" \"" + methodName + "\"");
        }
    }
    
  3. After the automation dll is injected in the application, the access to Visual Tree is pretty simple using Dispatcher and PresentationSources.

    public class Setup
    {
        public static bool Start()
        {
            Dispatcher dispatcher;
            if (Application.Current == null)
                dispatcher = Dispatcher.CurrentDispatcher;
            else
                dispatcher = Application.Current.Dispatcher;
    
            dispatcher.Invoke(AutomateApp);
            return true;
        }
    
        public static void AutomateApp()
        {
            Window root = null;
            foreach (PresentationSource presentationSource in PresentationSource.CurrentSources)
            {
                root = presentationSource.RootVisual as Window;
    
                if (root == null)
                    continue;
    
                if ("NordVPN ".Equals(root.Title))
                    break;
            }
    
  4. Getting access to VisualTree is easy, but identifying the controls is not that simple, as there are no automation-id(s), or name(s) that can uniquely identify these controls. But fortunately, as they are using MVVM, it is possible to identify them using the binding(s) attached with them.

    public static T GetChildWithPath<T>(this DependencyObject depObj, DependencyProperty property = null, string pathName = null) where T : DependencyObject
    {
        T toReturn = null;
    
        for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
        {
            var child = VisualTreeHelper.GetChild(depObj, i);
            bool pathNameMatch = (child is T) && child.IsPathNameMatch<T>(property, pathName);
            if (pathNameMatch)
            {
                toReturn = child as T;
                break;
            }
            else
                toReturn = GetChildWithPath<T>(child, property, pathName);
    
            if (toReturn != null)
                break;
        }
        return toReturn;
    }
    
  5. Once you have access to the controls, it is now possible to either manipulate their properties directly, or access their corresponding automation peers, and providers to automate these controls.

    var checkBoxNames = new[]
    {
        "CyberSec", "AutomaticUpdates", "AutoConnect",
        "StartOnStartup", "KillSwitch", "ShowNotifications",
        "StartMinimized", "ShowServerList", "ShowMap",
        "UseCustomDns", "ObfuscatedServersOnly"
    };
    
    foreach(var path in checkBoxNames)
    {
        var chkBox = settingsView.GetChildWithPath<CheckBox>(CheckBox.IsCheckedProperty, path);
        if(chkBox != null && chkBox.IsEnabled)
            chkBox.SimulateClick();
    }
    
完整的工作样例已上传到 Github 代码库

输入图像说明


1
我认为你的方法非常有趣,只是要想一下你的说法,即缺少AutomationId(在WPF中是Name属性)会导致控件对UI自动化客户端不可见。我看到许多自动化控件具有空的AutomationIds,当开发人员不费心为控件分配名称时。 - o_weisman
@o_weisman:我想我本可以选用更好的措辞。我的意思是使用自动化API唯一地识别或控制它们的能力。尽管WPF已知故意从自动化树中隐藏某些控件,如果这些属性不存在是为了提高性能。 - Sharada Gururaj
在这种情况下,我不得不使用可视化树,因为唯一的识别因素是与复选框相关联的绑定。 - Sharada Gururaj

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接