65.9K
CodeProject 正在变化。 阅读更多。
Home

以编程方式访问 WPF 应用程序的 XAML 和 UI,无需暴露 API 或自动化框架

starIconstarIconstarIconstarIconemptyStarIcon

4.00/5 (2投票s)

2016年12月4日

CPOL

8分钟阅读

viewsIcon

11923

通过访问视觉树来获得对应用程序 UI 的额外控制。

引言

撰写本文的原因源于我最近为一个客户进行的一个项目。我被指派编写 .NET 代码,该代码本质上作为插件由另一个应用程序运行。这个插件接口允许我们非常有限地通过脚本访问一个 API,该 API 对应用程序本身或 UI 的控制非常有限。

应用程序

在不透露过多信息(这是一个专有软件)的情况下,该应用程序基本加载并显示一张图片列表,用户需要通过单击每张单独的图片 来浏览这些图片,并使用所选图片上的信息执行特定操作。

请求

用户必须执行的每项任务都可以通过键盘完成,而无需使用鼠标,除非他们需要转到另一张图片,而这张图片必须被点击。 这显然给用户带来了真正的问题,打断了他们的工作流程。他们希望能够根据是否需要为特定图片执行任何工作来自动跳过该图片,而这只有在该特定图片被应用程序选中并加载后才能知道。

问题

虽然我可以通过脚本 API 暴露的信息知道选定的图片是否需要用户处理,但我无法通过代码选择另一张图片。没有公开的方法允许这种控制或操作。

顿悟

我对代表用户对每张图片所做工作的几个结构体有有限的访问权限。在 Visual Studio 中单步调试代码时,我注意到一种特定类型的对象有一个私有字段,类型为FrameworkElement,这让我意识到这个应用程序是一个 WPF 应用程序,这显然意味着(废话)XAML

背景

要真正理解我接下来要做的事情,你需要了解一点DotPeek,主要是如何

然而,所有这些都只是研究问题的一部分,其他程序也可以用不同的方式完成,例如ILSpy(拥有一个很棒的 BAML 反编译器)、.NET Reflector,或者基本上任何其他反编译器。

了解 WPF 中的路由事件也会有帮助,但你不需要任何非常深入的知识就能理解正在发生的事情。

研究“图片点击”

好的,我知道我可以通过 WPF 应用程序使用的PresentationFramework 程序集公开的一些方法来操作 UI 中的某些内容,但要了解具体哪些可行,哪些不可行,我需要更好地了解当用户点击下一张图片,或者任何一张图片时,应用程序实际上在做什么。

所以我打开了DotPeek,并按照上面列出的基本步骤,从进程资源管理器获取了运行应用程序的程序集,将其导出到一个项目,并将DotPeek设置为我的符号服务器,以便我可以单步调试代码并查看它在做什么。

经过一段时间的单步调试,我注意到,当用户点击一张图片时,他们实际点击的元素的类型是派生自基类Button 的一个类型,称为ImageButton 。所有此类型的按钮都存储在一个名为“ImageList”的ListView 中,点击其中一个实际上只是触发了一个标准的路由点击事件,这是可以通过编程方式模拟的。这听起来像个计划!

解决方案

所以,我的总体行动计划是

  1. 以某种方式访问和遍历 XAML,找到用户可以点击的图片,即ImageButton 类型的按钮。
  2. 弄清楚当前选中了哪张图片(可能通过包含的ListView 的一个属性,如SelectedItem SelectedIndex,或者可能检查是否有任何按钮包含 ListBoxItem.IsSelectedProperty 依赖属性,并且其值设置为 true)。
  3. 通过几种技术之一以编程方式执行路由点击事件,其中许多技术都显示在这个非常有见地的 StackOverflow 问题中。

访问 XAML

我脑海中有几种不同的方法可以做到这一点,其中一种是使用反射,通过 API 向我公开的一个结构体,连接到我之前发现的FrameworkElement 成员,然后使用PresentationCore 程序集中的静态VisualTreeHelper 类遍历 XAML。我尝试了一下,在我的脚本代码中实现了它,然后 bam! 完美奏效。

获取名为“_wpfElement的私有FrameworkElement 成员

var exposedObject = SomeStaticClass.GetExposedStructure("someObject");
FrameworkElement element = exposedObject.GetType()
                          .GetField("_wpfElement", BindingFlags.NonPublic | BindingFlags.Instance)
                          .GetValue(exposedObject) as FrameworkElement;

然而,一旦我得到了这个,我意识到我可能根本不需要这段代码。我假设我真正需要的是主窗口,以确保我总是从 XAML 的根部开始,以确保我只需要按一个方向遍历视觉树——向下。所以,相反,我记起了另一种更容易的方法来获取当前窗口作为FrameworkElement。

 private static FrameworkElement GetWindowRoot()
 {
     return Window.GetWindow(Application.Current.MainWindow);
 }

现在我有了GetWindowRoot() 方法,我就有了一个搜索视觉树的起点。

接下来,我需要一个递归搜索函数来填充我需要的按钮列表。

private static void FillButtonList(FrameworkElement currentElement, List<FrameworkElement> buttons)
{
    if (currentElement.GetType().ToString().ToLower().Contains("imagebutton"))
    {
        buttons.Add(currentElement);
    }
    for (int i = 0; i < VisualTreeHelper.GetChildrenCount(currentElement); ++i)
    {
        var next = VisualTreeHelper.GetChild(currentElement, i) as FrameworkElement;
        if (next != null)
        {
            FillButtonList(VisualTreeHelper.GetChild(currentElement, i) as FrameworkElement, buttons);
        }
    }
}

private static List<FrameworkElement> GetImageButtons(FrameworkElement rootElement)
{
    List<FrameworkElement> buttonList = new List<FrameworkElement>();
    FillNodeButtonList(rootElement, buttonList);
    return buttonList;
}

现在我已经获取了ImageButton 的列表,我可以继续弄清楚哪个被选中了。

获取选中的图片

因此,正如我上面提到的,我的第一个想法是检查包含的ListView SelectedItem SelectedIndex 属性,以找出当前选中了哪张图片,但是,没有成功。无论选中什么,SelectedItem 始终为 null,而SelectedIndex 始终为 -1

我的下一个想法是检查依赖属性 ListBoxItem.IsSelectedProperty ListViewItem.IsSelectedProperty,但这也没有帮助,因为没有任何按钮包含其中任何一个。

然而,我仍然坚信,ImageButton 类可能具有某种表示其选中状态的属性或字段,所以我和调试器一起单步调试了代码,并分析了通过上面显示的GetImageButtons函数检索到的ImageButton 对象的类成员。我正在寻找一个名为“selected”或“status”的成员,然后 voila! 我发现了一个名为“_activated” 的私有布尔字段。为了确保这是我想要的,我检查了每个按钮的_activated 字段值与我实际选中的ImageButton 进行对比,结果是正确的!代表选中的ImageButton 的对象对其_activated 字段的值为 true,而其余对象的值为 false

我写了一个函数来提取值为 true_activated 对象的索引,以指示选中了哪张图片。

public static int GetCurrentPageIndex()
{
    try
    {
        var rootElement = GetWindowRoot();
        var buttons = GetImageButtons(rootElement);
        for (int i = 0; i < buttons.Count; ++i)
        {
            var button = buttons[i];
            bool activated = 
                  button.GetType()
                  .GetField("_activated", BindingFlags.NonPublic | BindingFlags.Instance)
                  .GetValue(button) as bool;

            if (activated)
            {
                return i;
            }
        }
        return 0;
    }
    catch (Exception)
    {
        //generic placeholder exception
        throw new Exception();
    }
}

太棒了。现在我有了ImageButton 列表,并且可以判断哪个被选中了,我只需要创建一个方法来选择下一张图片,或者实际上是我想要的任何图片。

设置选中的图片

所以,正如我之前提到的,在 WPF 中以编程方式调用点击或其他 RoutedEvent 有几种方法,但是,我最喜欢的方法,因为它非常简单易懂,是前面提到的 StackOverflow 问题中建议的,使用 RaiseEvent 方法。

public static void MoveToNextPage()
{
    try
    {
        var currentlySelected = GetCurrentPageIndex();
        var newSelectionIndex = ++currentlySelected;
        SelectPage(newSelectionIndex);
    }
    catch (Exception)
    {
        //add in your own error handling as necessary
    }
}

public static void MoveToPreviousPage()
{
    try
    {
        var currentlySelected = GetCurrentPageIndex();
        var newSelectionIndex = currentlySelected == 0 ? currentlySelected : --currentlySelected;
        SelectPage(newSelectionIndex);
    }
    catch (Exception)
    {
        //add in your own error handling as necessary
    }
}
        
private static void SelectPage(int selectionIndex)
{            
    var rootElement = GetWindowRoot();
    var buttonList = GetImageButtons(rootElement);
    int currentlySelected = GetCurrentPageIndex();
    buttonList[selectionIndex].RaiseEvent(new RoutedEventArgs(ButtonBase.ClickEvent));
}

就这样!

对我来说,也希望对读者来说,这其中的主要收获是,我意识到我能够通过利用应用程序的当前窗口(或通过反射找到的任何可用的FrameworkElement ,如那些)作为FrameworkElement 来访问和利用应用程序的 XAML,从而访问视觉树,从中为我的代码打开了一个全新的功能世界。

我最终能够为用户节省大量时间,并通过将其设置为每当当前选中的图片不需要工作时自动选择下一张图片,从而避免打断他们的工作流程。它还为我解决了后来出现的问题提供了很大的空间,例如

  • 有时当加载一组图片时,他们可以提前知道第一张图片不需要任何工作,而现在,我不再需要点击到下一张图片,而是可以直接设置代码自动跳到第二张图片。
  • 我能够设置一个热键,让用户可以根据自己的意愿在图片之间来回切换,而无需离开键盘。

有没有更好的方法来做到这一点?

很有可能,但这对于当时的情况来说效果非常好。它允许我在代码中最小化反射,同时重现了我想要的确切用户行为。有几种不同的方法可以完成类似的事情,包括通过反射操作更多的私有类成员,或者以不同的方式触发 RoutedEvent,但这就像编程中的大多数事情一样,仅仅是工具箱中的另一个工具。 不同的东西可能效果更好——这只是当时对我来说有效的方法。

UIAutomation 框架可能也是一个不错的选择,但是,我喜欢上面的解决方案,因为它对我来说读起来更直接,而且对于任何出于任何原因都不想使用 UIAutomation 框架的人来说,这是一个很好的解决方案。

免责声明

我不认为这有什么不合法的,但是当处理专有软件时,一切都可能有点敏感,所以一如既往,如何以及是否使用这种技术取决于你。

历史

在此处保持您所做的任何更改或改进的实时更新。

© . All rights reserved.