이제 TreeWalker 클래스를 이용하여 UI 자동화 요소를 탐색기를 만들어 봅시다. UI 자동화 요소 탐색기는 프로세스 목록을 확인하여 선택하면 활성화 상태의 컨트롤 요소를 트리 뷰에 표시합시다. 그리고 TreeWalker의 정적 TreeWalker로 탐색하여 트리 뷰 항목을 변경하는 기능을 제공합시다. 또한 트리 뷰의 항목을 선택하였을 때 선택한 노드의 UI 자동화 요소를 레이블에 표시하고 자식, 형제, 부모를 찾아 레이블에 표시하는 기능을 제공합시다.
먼저 Windows Forms 응용 프로그램 프로젝트를 생성하시고 UI 자동화 관련 어셈블리를 참조 추가합니다. 그리고 폼에 컨트롤을 배치하세요.
No | 컨트롤 이름 | 컨트롤 형식 | No | 컨트롤 이름 | 컨트롤 형식 |
1 | tv_ae | TreeView | 7 | lb_ae_info | Label |
2 | btn_refresh | Button | 8 | btn_first_child | Button |
3 | lbox_process | ListBox | 9 | btn_next | Button |
4 | btn_find_row | Button | 10 | btn_prev | Button |
5 | btn_find_contrl | Button | 11 | btn_last_child | Button |
6 | btn_find_content | Button | 12 | btn_parent | Button |
[표 4.1] 컨트롤
프로세스 목록을 리스트 상자에 표시하고 선택한 프로세스 정보를 기억할 수 있게 래핑한 프로세스 클래스를 추가합시다.
WrapProcesss 클래스에 관한 설명은 초점 제어기에서 다루었습니다. 참고하시기 바랍니다.
using System; using System.Diagnostics; using System.Windows.Automation; namespace UsingTreeWalker { public class WrapProcess { public Process Process{ get; private set; } public string Title { get { return Process.ProcessName + ":" + Process.MainWindowTitle; } } public AutomationElement RootElement { get { if (Process.MainWindowHandle == IntPtr.Zero){ return null; } return AutomationElement.FromHandle(Process.MainWindowHandle); } } public WrapProcess(Process process){ Process = process; } public override string ToString(){ return Title; } } }
[소스 4.1] WrapProcess.cs
그리고 AutomationElement의 정보를 표시할 때 컨트롤 이름과 지역화 컨트롤 타입을 조합하여 표시하는 래퍼 클래스를 정의하세요. 이 부분도 초점 제어기에서 설명했던 부분입니다.
using System.Windows.Automation; namespace UsingTreeWalker { public class WrapAE { public AutomationElement AE{ get; private set; } public string Name { get{ return AE.Current.Name; } } public string ControlType { get{ return AE.Current.LocalizedControlType; } } public string Boundary { get{ return AE.Current.BoundingRectangle.ToString(); } } public WrapAE(AutomationElement ae) { AE = ae; } public override string ToString() { return ControlType+":"+Name; } } }
[소스 4.2] WrapAE.cs
이번에는 메인 폼에서 사용자의 명령을 받았을 때 실제 작업을 처리하는 클래스를 정의합시다. 클래스 이름은 UISearcher로 할게요.
public class UISearcher
멤버 필드로 래핑한 자동화 요소가 있고 생성자에서 입력 받은 값으로 기억하게 합시다. UI 자동화 요소 탐색기에서는 특정 프로세스를 선택하였을 때 메인 창의 UI 자동화 요소를 기준으로 탐색할 것입니다. 이를 위해서 선택한 프로세스의 메인 창의 UI 자동화 요소를 래핑한 자동화 요소를 생성자의 입력 인자로 전달받아 멤버 필드 wae를 설정합니다.
WrapAE wae; public UISearcher(WrapAE wae) { this.wae = wae; }
초기에 프로세스를 선택하면 선택한 프로세스의 활성화 컨트롤 목록을 트리 뷰에 표시합니다. 이를 위해 활성화 컨트롤을 탐색해 TreeNode에 추가하여 반환하는 기능을 제공합시다.
public TreeNode FindEnalbleControl() { 먼저 멤버 필드 wae의 문자열을 입력 인자로 TreeNode를 생성합니다. 이 노드는 탐색한 자동화 요소를 보관한 TreeNode들로 구성한 트리의 루트 노드입니다. TreeNode tn = new TreeNode(wae.ToString()); 트리 뷰에서 특정 TreeNode를 선택하였을 때 자동화 요소 속성을 레이블에 표시할 것입니다. 이를 위해 트리 노드의 Tag에 래핑한 자동화 요소를 설정합니다. tn.Tag = wae; 활성화 요소를 탐색하여 트리 노드에 매다는 기능을 호출합니다. WalkEnabledElements(wae, tn); return tn; }
private void WalkEnabledElements(WrapAE wae, TreeNode tn) { 컨트롤 여부를 판단하는 속성이 참인 속성 컨디션 개체를 생성합니다. Condition cond1 = new PropertyCondition( AutomationElement.IsControlElementProperty, true); 활성화 상태가 참인 속성 컨디션 개체를 생성합니다. Condition cond2 = new PropertyCondition( AutomationElement.IsEnabledProperty, true); 두 개의 컨디션을 인자로 AndCondition 개체를 생성하여 이를 입력 인자로 TreeWalker 개체를 생성합니다. TreeWalker walker = new TreeWalker(new AndCondition(cond1, cond2)); 이제 생성한 TreeWalker 개체와 래핑한 자동화 요소, 트리 노드를 입력 인자로 탐색합니다. WalkTree(walker, wae, tn); }
private void WalkTree(TreeWalker walker, WrapAE wae, TreeNode tn) { 입력 인자로 전달받은 자동화 요소의 첫번째 자식 요소를 찾습니다. AutomationElement child = walker.GetFirstChild(wae.AE); 자식 자동화 요소가 null이 아니면 다음을 반복합니다. while (child != null) { 자식 자동화 요소를 래핑한 개체를 생성합니다. WrapAE cae = new WrapAE(child); 래핑한 개체의 문자열을 입력 인자로 트리 노드를 생성합니다. TreeNode ctn = tn.Nodes.Add(cae.ToString()); 트리 노드의 Tag 속성에 래핑한 개체를 설정합니다. ctn.Tag = cae; 재귀적으로 WalkTree를 호출합니다. WalkTree(walker,cae, ctn); 자식 개체의 형제 자동화 요소를 찾습니다. child = walker.GetNextSibling(child); } }
전체 자동화 요소 트리를 TreeNode로 만들어주는 기능과 전체 컨트롤 자동화 요소 트리 및 전체 컨텐츠 자동화 요소를 TreeNode로 만들어 주는 기능을 제공합시다. public TreeNode FindRawTree() { MakeNodeWithWalkTree 메서드의 입력 인자를 RawViewWalker를 전달합니다. return MakeNodeWithWalkTree(TreeWalker.RawViewWalker); }
public TreeNode FindControlTree() { MakeNodeWithWalkTree 메서드의 입력 인자를 ControlViewWalker를 전달합니다. return MakeNodeWithWalkTree(TreeWalker.ControlViewWalker); } public TreeNode FindContentTree() { MakeNodeWithWalkTree 메서드의 입력 인자를 ContentViewWalker를 전달합니다. return MakeNodeWithWalkTree(TreeWalker.ContentViewWalker); }
MakeNodeWithWalkTree 메서드를 구현합시다. private TreeNode MakeNodeWithWalkTree(TreeWalker walker) { 멤버 필드 wae 개체의 문자열을 입력 인자로 TreeNode를 생성합니다. 이 TreeNode는 폼에서 트리 뷰의 루트 노드로 등록할 것입니다. TreeNode tn = new TreeNode(wae.ToString()); 트리 노드의 Tag에 wae 개체를 등록합니다. tn.Tag = wae; WalkTree를 이용하여 자동화 트리를 트리 노드를 구성합니다. WalkTree(walker, wae, tn); return tn; }
래핑한 자동화 요소의 첫번재 자식을 찾는 기능도 필요합니다. 탐색 기능들은 모두 TreeWalker 의 정적 멤버인 RawViewWalker 개체를 사용할게요. public static WrapAE FindFirstChild(WrapAE wae) { TreeWalker tw = TreeWalker.RawViewWalker; TreeWalker의 GetFirstChild 메서드로 첫번째 자식 요소를 찾습니다. AutomationElement ae = tw.GetFirstChild(wae.AE); 찾지 못하면 입력 인자로 받은 개체를 반환합시다. if (ae == null){ return wae; } 찾았으면 찾은 자동화 요소를 래핑한 개체를 생성하여 반환합니다. return new WrapAE(ae); }
같은 원리로 나머지 탐색 기능을 구현합니다.
public static WrapAE FindNext(WrapAE wae) { TreeWalker tw = TreeWalker.RawViewWalker; AutomationElement ae = tw.GetNextSibling(wae.AE); if (ae == null){ return wae; } return new WrapAE(ae); } public static WrapAE FindPrev(WrapAE wae) { TreeWalker tw = TreeWalker.RawViewWalker; AutomationElement ae = tw.GetPreviousSibling(wae.AE); if (ae == null){ return wae; } return new WrapAE(ae); } public static WrapAE FindLastChild(WrapAE wae) { TreeWalker tw = TreeWalker.RawViewWalker; AutomationElement ae = tw.GetLastChild(wae.AE); if (ae == null){ return wae; } return new WrapAE(ae); } public static WrapAE FindParent(WrapAE wae) { TreeWalker tw = TreeWalker.RawViewWalker; AutomationElement ae = tw.GetParent(wae.AE); if (ae == null){ return wae; } return new WrapAE(ae); }
using System.Windows.Automation; using System.Windows.Forms; namespace UsingTreeWalker { public class UISearcher { WrapAE wae; public UISearcher(WrapAE wae) { this.wae = wae; } public TreeNode FindEnalbleControl() { TreeNode tn = new TreeNode(wae.ToString()); tn.Tag = wae; WalkEnabledElements(wae, tn); return tn; } public TreeNode FindRawTree() { return MakeNodeWithWalkTree(TreeWalker.RawViewWalker); } public TreeNode FindControlTree() { return MakeNodeWithWalkTree(TreeWalker.ControlViewWalker); } public TreeNode FindContentTree() { return MakeNodeWithWalkTree(TreeWalker.ContentViewWalker); } private TreeNode MakeNodeWithWalkTree(TreeWalker walker) { TreeNode tn = new TreeNode(wae.ToString()); tn.Tag = wae; WalkTree(walker, wae, tn); return tn; } private void WalkTree(TreeWalker walker, WrapAE wae, TreeNode tn) { AutomationElement child = walker.GetFirstChild(wae.AE); while (child != null) { WrapAE cae = new WrapAE(child); TreeNode ctn = tn.Nodes.Add(cae.ToString()); ctn.Tag = cae; WalkTree(walker,cae, ctn); child = walker.GetNextSibling(child); } } private void WalkEnabledElements(WrapAE wae, TreeNode tn) { Condition cond1 = new PropertyCondition( AutomationElement.IsControlElementProperty, true); Condition cond2 = new PropertyCondition( AutomationElement.IsEnabledProperty, true); TreeWalker walker = new TreeWalker(new AndCondition(cond1, cond2)); WalkTree(walker, wae, tn); } public static WrapAE FindFirstChild(WrapAE wae) { TreeWalker tw = TreeWalker.RawViewWalker; AutomationElement ae = tw.GetFirstChild(wae.AE); if (ae == null){ return wae; } return new WrapAE(ae); } public static WrapAE FindNext(WrapAE wae) { TreeWalker tw = TreeWalker.RawViewWalker; AutomationElement ae = tw.GetNextSibling(wae.AE); if (ae == null) { return wae; } return new WrapAE(ae); } public static WrapAE FindPrev(WrapAE wae) { TreeWalker tw = TreeWalker.RawViewWalker; AutomationElement ae = tw.GetPreviousSibling(wae.AE); if (ae == null) { return wae; } return new WrapAE(ae); } public static WrapAE FindLastChild(WrapAE wae) { TreeWalker tw = TreeWalker.RawViewWalker; AutomationElement ae = tw.GetLastChild(wae.AE); if (ae == null) { return wae; } return new WrapAE(ae); } public static WrapAE FindParent(WrapAE wae) { TreeWalker tw = TreeWalker.RawViewWalker; AutomationElement ae = tw.GetParent(wae.AE); if (ae == null) { return wae; } return new WrapAE(ae); } } }
[소스 4.3] UISearcher.cs
이제 폼을 구현할 차례입니다.
먼저 프로세스 새로고침 버튼에 클릭 이벤트 핸들러를 추가합니다.
private void btn_refresh_Click(object sender, EventArgs e) { 프로세스 ListBox의 항목을 지웁니다. lbox_process.Items.Clear(); 프로세스 목록을 구합니다. Process[] processes = Process.GetProcesses(); 프로세스 목록의 각 프로세스마다 다음을 반복합니다. foreach (Process pro in processes) { 만약 프로세스의 MainWindowHandle이 있으면 프로세스를 래핑한 개체를 생성하여 프로세스 ListBox의 항목에 추가합니다. if (pro.MainWindowHandle != IntPtr.Zero) { lbox_process.Items.Add(new WrapProcess(pro)); } } }
프로세스 ListBox의 항목 변경 이벤트 핸들러를 추가합니다. private void lbox_process_SelectedIndexChanged(object sender, EventArgs e) { 자동화 요소 TreeView의 노드를 제거하고 선택한 항목의 개체를 구합니다. tv_ae.Nodes.Clear(); WrapAE wae = SelectedWrapAE(); 만약 선택한 항목의 개체가 없다면 기능을 종료합니다. if (wae == null){ return; } 선택한 항목의 개체를 인자로 UISearcher 개체를 생성합니다. UISearcher uis = new UISearcher(wae); 생성한 개체의 FindEnableControl 메서드를 호출합니다. FindEnalbeControl 메서드에서는 활성화 상태의 컨트롤들로 구성한 TreeNode를 만들어 반환합니다. 그리고 반환받은 개체를 입력 인자로 TreeView의 루트 노드를 변경합니다. ChangeRootNode(uis.FindEnalbleControl()); uis = null; }
private void ChangeRootNode(TreeNode rootnode) { ChageRootNode 메서드에서는 트리 뷰의 Node 컬렉션에 root 노드를 추가한 후 노드를 펼치게 합니다. 그리고 스크롤 위치를 맨 위로 위치하도록 선택 노드를 rootnode로 설정합니다. tv_ae.Nodes.Add(rootnode); tv_ae.ExpandAll(); tv_ae.SelectedNode = rootnode; }
프로세스 목록에서 선택 항목의 루트 자동화 요소를 반환하는 메서드를 정의합시다.
WrapAE SelectedWrapAE()
{
ListBox의 선택 항목이 없다면 SelectedIndex 속성 값은 -1입니다.
if (lbox_process.SelectedIndex == -1)
{
return null;
}
ListBox의 선택 항목을 WrapProcess로 참조합니다.
WrapProcess wp = lbox_process.SelectedItem as WrapProcess;
참조한 프로세스의 루트 자동화 요소를 가져옵니다.
AutomationElement ae = wp.RootElement;
루트 요소를 래핑한 개체를 반환합니다.
return new WrapAE(ae);
}
전체 찾기 버튼을 클릭했을때 이벤트 핸들러를 추가하세요.
private void btn_find_raw_Click(object sender, EventArgs e) { 트리 뷰의 노드 컬렉션을 비워줍니다. tv_ae.Nodes.Clear(); 선택 항목의 개체를 구합니다. WrapAE wae = SelectedWrapAE(); 선택 항목이 없으면 기능을 종료합니다. if (wae == null) { return; } 선택 개체를 입력 인자로 UISearcher 개체를 생성합니다. UISearcher uis = new UISearcher(wae); 생성한 개체의 FindRawTree 메서드를 이용하여 전체 목록을 포함한 TreeNode를 구하여 루트 노드를 변경합니다. ChangeRootNode(uis.FindRawTree()); 탐색에만 사용하는 UISearcher 개체는 더 이상 필요없으니 uis 변수에 null을 설정합니다. uis = null; }
컨트롤 계층 보기 버튼 클릭 이벤트 핸들러를 추가하세요. 내부 기능은 전체 보기 버튼 클릭 이벤트 핸들러와 비슷합니다. 차이가 있는 부분은 UISearch 개체의 FindControlTree 메서드를 호출한다는 점 뿐입니다.
private void btn_find_contrl_Click(object sender, EventArgs e) { tv_ae.Nodes.Clear(); WrapAE wae = SelectedWrapAE(); if (wae == null){ return; } UISearcher uis = new UISearcher(wae); ChangeRootNode(uis.FindControlTree()); uis = null; }
컨텐츠 계층 보기 버튼 클릭 이벤트 핸들러를 추가하세요. 내부 기능은 전체 보기 버튼 클릭 이벤트 핸들러와 비슷합니다. 차이가 있는 부분은 UISearch 개체의 FindContentTree 메서드를 호출한다는 점 뿐입니다.
private void btn_find_content_Click(object sender, EventArgs e) { tv_ae.Nodes.Clear(); WrapAE wae = SelectedWrapAE(); if (wae == null){ return; } UISearcher uis = new UISearcher(wae); ChangeRootNode(uis.FindContentTree()); uis = null; }
이번에는 탐색 기능들을 구현합시다.
먼저 트리 뷰의 선택 항목을 변경했을 때의 이벤트 핸들러를 추가하세요.
private void tv_ae_AfterSelect(object sender, TreeViewEventArgs e) { 선택한 노드를 구합니다. TreeNode tn = e.Node; 선택한 노드가 없으면 기능을 종료합니다. if (tn == null) { return; } 선택한 노드의 tag에 설정한 개체를 참조합니다. WrapAE wae = tn.Tag as WrapAE; 참조한 개체의 ToString 메서드를 호출하여 자동화 요소 레이블의 Text 속성을 설정합니다. lb_ae_info.Text = wae.ToString(); 레이블의 Tag 속성에 참조한 개체를 설정합니다. lb_ae_info.Tag = wae; }
레이블 테그에 설정한 자동화 요소와 관련 요소를 탐색하는 기능들을 구현합시다. 먼저 첫번째 자식 찾기 클릭 이벤트 핸들러를 추가합니다.
private void btn_first_child_Click(object sender, EventArgs e) { 자동화 요소 레이블의 Tag 속성에 설정한 요소를 참조합니다. WrapAE wae = lb_ae_info.Tag as WrapAE; 참조한 개체가 없으면 기능을 종료합니다. if (wae == null) { return; } UISearcher의 정적 메서드 FindFirstChild 메서드를 이용하여 첫번째 자식 요소를 탐색합니다. wae = UISearcher.FindFirstChild(wae); 탐색한 요소의 ToString 메서드로 자동화 요소 정보 레이블의 Text 속성을 설정합니다. lb_ae_info.Text = wae.ToString(); 레이블의 Tag 속성도 탐색한 요소로 설정합니다. lb_ae_info.Tag = wae; }
나머지 탐색 버튼의 클릭 이벤트 핸들러도 추가합니다. 그리고 탐색 목적에 맞는 UISearcher의 정적 메서드로 탐색후에 자동화 요소 정보 레이블의 속성을 변경합니다. 구현 방법은 첫번째 자식 탐색 버튼 클릭 이벤트 핸들러와 비슷합니다.
private void btn_next_Click(object sender, EventArgs e) { WrapAE wae = lb_ae_info.Tag as WrapAE; if (wae == null) { return; } wae = UISearcher.FindNext(wae); lb_ae_info.Text = wae.ToString(); lb_ae_info.Tag = wae; } private void btn_prev_Click(object sender, EventArgs e) { WrapAE wae = lb_ae_info.Tag as WrapAE; if (wae == null){ return; } wae = UISearcher.FindPrev(wae); lb_ae_info.Text = wae.ToString(); lb_ae_info.Tag = wae; } private void btn_last_child_Click(object sender, EventArgs e) { WrapAE wae = lb_ae_info.Tag as WrapAE; if (wae == null) { return; } wae = UISearcher.FindLastChild(wae); lb_ae_info.Text = wae.ToString(); lb_ae_info.Tag = wae; } private void btn_parent_Click(object sender, EventArgs e) { WrapAE wae = lb_ae_info.Tag as WrapAE; if (wae == null) { return; } wae = UISearcher.FindParent(wae); lb_ae_info.Text = wae.ToString(); lb_ae_info.Tag = wae; }
using System; using System.Windows.Forms; using System.Diagnostics; using System.Windows.Automation; namespace UsingTreeWalker { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void btn_refresh_Click(object sender, EventArgs e) { lbox_process.Items.Clear(); Process[] processes = Process.GetProcesses(); foreach (Process pro in processes) { if (pro.MainWindowHandle != IntPtr.Zero) { lbox_process.Items.Add(new WrapProcess(pro)); } } } private void lbox_process_SelectedIndexChanged(object sender, EventArgs e) { tv_ae.Nodes.Clear(); WrapAE wae = SelectedWrapAE(); if (wae == null) { return; } UISearcher uis = new UISearcher(wae); ChangeRootNode(uis.FindEnalbleControl()); uis = null; } private void btn_find_raw_Click(object sender, EventArgs e) { tv_ae.Nodes.Clear(); WrapAE wae = SelectedWrapAE(); if (wae == null) { return; } UISearcher uis = new UISearcher(wae); ChangeRootNode(uis.FindRawTree()); uis = null; } private void btn_find_contrl_Click(object sender, EventArgs e) { tv_ae.Nodes.Clear(); WrapAE wae = SelectedWrapAE(); if (wae == null) { return; } UISearcher uis = new UISearcher(wae); ChangeRootNode(uis.FindControlTree()); uis = null; } private void btn_find_content_Click(object sender, EventArgs e) { tv_ae.Nodes.Clear(); WrapAE wae = SelectedWrapAE(); if (wae == null) { return; } UISearcher uis = new UISearcher(wae); ChangeRootNode(uis.FindContentTree()); uis = null; } private void ChangeRootNode(TreeNode rootnode) { tv_ae.Nodes.Add(rootnode); tv_ae.ExpandAll(); tv_ae.SelectedNode = rootnode; } WrapAE SelectedWrapAE() { if (lbox_process.SelectedIndex == -1) { return null; } WrapProcess wp = lbox_process.SelectedItem as WrapProcess; AutomationElement ae = wp.RootElement; return new WrapAE(ae); } private void tv_ae_AfterSelect(object sender, TreeViewEventArgs e) { TreeNode tn = e.Node; if (tn == null) { return; } WrapAE wae = tn.Tag as WrapAE; lb_ae_info.Text = wae.ToString(); lb_ae_info.Tag = wae; } private void btn_first_child_Click(object sender, EventArgs e) { WrapAE wae = lb_ae_info.Tag as WrapAE; if (wae == null) { return; } wae = UISearcher.FindFirstChild(wae); lb_ae_info.Text = wae.ToString(); lb_ae_info.Tag = wae; } private void btn_next_Click(object sender, EventArgs e) { WrapAE wae = lb_ae_info.Tag as WrapAE; if (wae == null){ return; } wae = UISearcher.FindNext(wae); lb_ae_info.Text = wae.ToString(); lb_ae_info.Tag = wae; } private void btn_prev_Click(object sender, EventArgs e) { WrapAE wae = lb_ae_info.Tag as WrapAE; if (wae == null){ return; } wae = UISearcher.FindPrev(wae); lb_ae_info.Text = wae.ToString(); lb_ae_info.Tag = wae; } private void btn_last_child_Click(object sender, EventArgs e) { WrapAE wae = lb_ae_info.Tag as WrapAE; if (wae == null){ return; } wae = UISearcher.FindLastChild(wae); lb_ae_info.Text = wae.ToString(); lb_ae_info.Tag = wae; } private void btn_parent_Click(object sender, EventArgs e) { WrapAE wae = lb_ae_info.Tag as WrapAE; if (wae == null){ return; } wae = UISearcher.FindParent(wae); lb_ae_info.Text = wae.ToString(); lb_ae_info.Tag = wae; } } }
[소스 4.4] Form1.cs