微软的TAF技术,可以实现自动化测试,或是对其他进程进行UI操作。https://blog.youkuaiyun.com/jfyy/article/details/80700977
参考:https://www.codeproject.com/Articles/172391/UIAutomation-Coded-UI-Tests-AutomationPeer-and-WPF
https://docs.microsoft.com/en-us/dotnet/framework/wpf/controls/ui-automation-of-a-wpf-custom-control
为了在WPF里支持这套技术,微软使用了Peer对象,并为其标准控件(button,textblock)制作了对应的Peer类,例如ButtonAutomationPeer。并在控件的protected override AutomationPeer OnCreateAutomationPeer()方法里,实例化对应的Peer类。
但如果是我们制作的Customize控件,或使用第三方的控件,因为没有对应地制作Peer类,就不能支持TAF。下面的例子就是我在研究如何为第三方ActiPro的TreeListBox控件,制作Peer的过程。
1,先如下派生自己的TreeListBox,并实现OnCreateAutomationPeer()
public class AutomationTreeListBox : TreeListBox
{
protected override AutomationPeer OnCreateAutomationPeer()
{
return new TreeListAutomationPeer(this);
}
}
只要我们能够制作出TreeListAutomationPeer,就能完美支持TAF。
2, TreeListAutomationPeer基类的选择
没必要重复做轮子,选择恰当的AutomationPeer基类会减少很多工作量。
如果是制作的是一个简单Control, 可使用FrameworkElementAutomationPeer。
如果是制作一个TabControl,可使用TabControlAutomationPeer。
所以我第一时间想选择TreeViewAutomationPeer作为基类,可第三方ActiPro的TreeListBox控件是派生于ItemsControl,而TreeViewAutomationPeer构造函数需要传入的是TreeView控件。因此TreeViewAutomationPeer作为基类是不可能的。故最终选择ItemsControlAutomationPeer作为我们的基类。
3,主要的Override方法:GetClassNameCore()和GetChildrenCore()。
尤其是GetChildrenCore()。 因为树结构,每个节点的GetChildrenCore如果都能返回正确的List<AutomationPeer>,我们的工作基本就完成了。
我的思路是:
- 为每个TreeNode定义TreeNodeAutomationPeer类,其基类是ItemsControlAutomationPeer。
- 重写TreeNodeAutomationPeer类的GetChildrenCore()方法,根据Children数据先找到所有Children的UIElment。针对每一个UIElment,var peer = new TreeNodeAutomationPeer(treeNode, ctrl),最后将peer加入list后返回。
采用上面的思路,找到Root节点,并找到其UIElement:rootCtrl。按上面的方法return new List<TreeNodeAutomationPeer>() { new TreeNodeAutomationPeer(Root, rootCtrl)。该方法递会用归方式完成tree所有节点AutomationPeer的创建。
问题出来了!!!
第三方节点的类型是TreeListBoxItem,它派生于ContentControl,不是ItemsControl。这样无法用它来创建ItemsControlAutomationPeer。(一般来说,针对有多个子节点的控件,一般都会采用ItemsControl。)
所以,后来我只能用var peer = new FrameworkElementAutomationPeer(ctrl);来完成对根节点的构建。子节点无法再构建了。
(网上有这种写法,var peer = UIElementAutomationPeer.CreatePeerForElement(MyOwner.PART_Header); 但peer返回null,原因不明,放弃使用。)
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Automation.Peers;
using System.Windows.Controls;
using System.Windows.Media;
using ActiproSoftware.Windows.Controls.Grids;
using RA.CCW.OrganizerControl;
namespace RA.CCW.ControllerOrganizer.TreeListControl
{
public class TreeListAutomationPeer : ItemsControlAutomationPeer
{
public TreeListAutomationPeer(AutomationTreeListBox owner) : base(owner)
{
}
protected override string GetClassNameCore()
{
return "AutomationTreeListBox";
}
public override object GetPattern(PatternInterface patternInterface)
{
if (patternInterface == PatternInterface.ExpandCollapse)
{
return this;
}
return base.GetPattern(patternInterface);
}
protected override ItemAutomationPeer CreateItemAutomationPeer(object item)
{
return null;
}
// todo!!!!
protected override List<AutomationPeer> GetChildrenCore()
{
var ret = new List<AutomationPeer>();
if(TreeList.RootItem != null)
{
var root = TreeList.RootItem as TreeNodeModel;
if (root != null)
{
var peer = GetTreeNodeAutomationPeer(root);
if(peer != null)
ret.Add(peer);
}
}
return ret;
}
protected AutomationPeer GetTreeNodeAutomationPeer(TreeNodeModel treeNode)
{
var ctrl = SearchVisualTree<TreeListBoxItem>(TreeList, x => x.Name == treeNode.Name);
if (ctrl != null)
{
var peer = new FrameworkElementAutomationPeer(ctrl);
//var peer = new ItemsControlAutomationPeer(ctrl); // can't create ItemsControlAutomationPeer...
return peer;
}
return null;
}
// need???
protected override AutomationControlType GetAutomationControlTypeCore()
{
return AutomationControlType.Tree;
}
private AutomationTreeListBox TreeList
{
get { return (AutomationTreeListBox)base.Owner; }
}
private T SearchVisualTree<T>(DependencyObject tarElem) where T : DependencyObject
{
var count = VisualTreeHelper.GetChildrenCount(tarElem);
if (count == 0)
return null;
for (int i = 0; i < count; ++i)
{
var child = VisualTreeHelper.GetChild(tarElem, i);
if (child != null && child is T)
{
return (T)child;
}
else
{
var res = SearchVisualTree<T>(child);
if (res != null)
{
return res;
}
}
}
return null;
}
private T SearchVisualTree<T>(DependencyObject tarElem, Func<T, bool> predicate) where T : DependencyObject
{
var count = VisualTreeHelper.GetChildrenCount(tarElem);
if (count == 0)
return null;
for (int i = 0; i < count; ++i)
{
var child = VisualTreeHelper.GetChild(tarElem, i);
if (child != null && child is T && predicate(child as T))
{
return (T)child;
}
else
{
var res = SearchVisualTree<T>(child);
if (res != null)
{
return res;
}
}
}
return null;
}
private TextBlock SearchVisualTree(DependencyObject tarElem, string name)
{
var count = VisualTreeHelper.GetChildrenCount(tarElem);
if (count == 0)
return null;
for (int i = 0; i < count; ++i)
{
var child = VisualTreeHelper.GetChild(tarElem, i);
if (child != null && child is TreeListBoxItem && ((TreeListBoxItem)child).Name == name)
{
return child as TextBlock;
}
else
{
var res = SearchVisualTree(child, name);
if (res != null)
{
return res;
}
}
}
return null;
}
}
class TreeNodeAutomationPeer : ItemsControlAutomationPeer
{
}
public class AutomationTreeListBox : TreeListBox
{
protected override AutomationPeer OnCreateAutomationPeer()
{
return new TreeListAutomationPeer(this);
}
}
}
另外:
我们用Inspect.exe来观看控件的层次结构。当然也可以来验证实现Automation是否成功。
另外,上面说得都是WPF程序。如果对应的是Winform的程序,微软使用的是Implement Pattern Providers。这个尚未研究。