이번에는 클라이어트용 UI 자동화 이벤트를 이용하여 자식 요소를 추가하거나 제거할 때의 이벤트를 구독하는 구조 변경 감시자 응용 프로그램을 만들어 봅시다.
구조 변경 감시자 응용에서는 프로세스 목록 새로 고침 버튼을 클릭하면 리스트 박스에 프로세스 목록을 표시합니다. 프로세스 목록 리스트 상자에서 항목을 선택하면 선택한 프로세스의 자동화 요소를 가운데 리스트 상자에 표시합니다. 그리고 선택한 프로세스의 메인 창의 자식 요소를 추가하거나 제거하는 이벤트가 발생하면 이를 마지막 리스트 상자에 표시합니다.
이를 위해 UI 자동화 이벤트 중에 구조 변경 이벤트 핸들러를 등록하고 이벤트 인자를 이용하는 실습을 할 것입니다.
먼저 컨트롤을 배치합시다.
번호 | 컨트롤 타입 | 컨트롤 명 | 설명 |
1 | Button | btn_refresh | 프로세스 목록 새로고침 버튼 |
2 | ListBox | lbox_process | 프로세스 리스트 박스 |
3 | ListBox | lbox_ae | 자동화 요소 리스트 박스 |
4 | ListBox | lbox_event | 자동화 이벤트 리스트 박스 |
[표 6.1] Form1의 컨트롤 배치
이 프로젝트에서도 프로세스를 래핑하고 자동화 요소를 래핑하는 부분이 필요합니다. 특히 프로세스를 래핑하는 부분은 5장 InvokePattern 요소 제어기에서 만든 것과 차이가 없습니다. 여기에서는 프로세스 래핑 클래스에 관한 설명은 하지 않겠습니다.
그리고 자동화 요소를 래핑하는 부분은 앞에서 했었던 부분과 비슷하지만 구조 변경 이벤트 중에 자식을 삭제할 때 삭제한 자식의 자동화 요소 정보가 null로 전달받아 이를 확인하기 쉽게 약간의 변경 작업이 필요합니다. 이 부분만 별도로 설명하고 넘어가기로 할게요.
public class WrapAE { 먼저 구조 변경 이벤트를 멤버 이벤트로 캡슐화합니다. public event StructureChangedEventHandler StructureChanged; ...중략... 이름 속성은 삭제한 요소의 이름을 얻어오는 부분에서 예외가 발생할 수 있어서 예외 처리를 추가합시다. public string Name { get { if (AE == null){ return string.Empty; } try { return AE.Current.Name; } catch { return "N/A"; } } } ... 중략... public WrapAE(AutomationElement ae) { AE = ae; if (ae != null) { 생성자 메서드에서는 구조 변경 이벤트 핸들러를 등록합니다. Automation.AddStructureChangedEventHandler(ae, TreeScope.Element, new StructureChangedEventHandler(OnStructureChanged)); } }
구조 변경 이벤트 핸드러를 작성합니다.
private void OnStructureChanged(object sender, StructureChangedEventArgs e) { if (StructureChanged != null) { 이벤트 멤버가 null이 아니면 이벤트를 전달합니다. sender 부분은 래핑한 멤버 AE를 전달합시다. StructureChanged(AE, e); } } ...중략... }
다음은 WrapAE.cs 소스 파일과 WrapProcess.cs 소스 파일의 코드입니다.
using System.Windows.Automation; namespace Ex_구조변경감시자 { public class WrapAE { public event StructureChangedEventHandler StructureChanged; public AutomationElement AE{ get; private set; } public string Name { get { if (AE == null){ return string.Empty; } try { return AE.Current.Name; } catch { return "N/A"; } } } public string ControlType { get { if (AE == null) { return string.Empty; } return AE.Current.LocalizedControlType; } } public WrapAE(AutomationElement ae) { AE = ae; if (ae != null) { Automation.AddStructureChangedEventHandler(ae, TreeScope.Element, new StructureChangedEventHandler(OnStructureChanged)); } } private void OnStructureChanged(object sender, StructureChangedEventArgs e) { if (StructureChanged != null) { StructureChanged(AE, e); } } public override string ToString() { return ControlType + ":" + Name; } } }
[소스 6.1] WrapAE.cs
using System; using System.Windows.Automation; using System.Diagnostics; namespace Ex_구조변경감시자 { 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; } } }
[소스 6.2] WrapProcess.cs
프로세스 목록 새로고침 클릭 이벤트 핸들러를 추가합시다.
private void btn_refresh_Click(object sender, EventArgs e) { 프로세스 목록과 이벤트 목록을 모두 지웁니다. lbox_process.Items.Clear(); lbox_event.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) { 선택한 프로세스의 자동화 요소를 조사하여 리스트 박스에 설정하는 작업을 수행해야 합니다. 그런데 이 부분은 구조 변경이 발생할 때도 수행해야 하므로 별도의 메서드로 정의합시다. SetAEList(); }
구조 변경이 발생하여 자동화 요소 리스트 박스 항목을 설정할 때는 크로스 스레드 문제가 발생합니다. 이를 위해 대리자를 정의하세요.
delegate void MySetListDele();
그리고 자동화 요소 목록을 리스트 박스의 항목에 설정하는 메서드를 구현합시다.
private void SetAEList() { 먼저 실행하는 스레드가 컨트롤을 소유한 스레드인지 확인합니다. if (this.InvokeRequired) { 만약 서로 다른 스레드면 컨트롤의 Invoke 메서드를 이용하여 컨트롤 소유 스레드에서 작업을 대행할 수 있게 처리합니다. try { this.Invoke(new MySetListDele(SetAEList)); } catch{ } } else { 먼저 기존에 있는 자동화 요소 목록을 지워줍니다. lbox_ae.Items.Clear(); 그리고 등록한 모든 UI 자동화 이벤트를 제거합니다. Automation.RemoveAllEventHandlers(); 선택한 프로세스의 자동화 요소를 구합니다. 이 부분은 별도의 메서드로 작성할게요. WrapAE wae = SelectedWrapAE(); 만약 선택한 자동화 요소가 null이면 메서드를 종료합니다. if (wae == null){ return; } 선택한 자동화 요소와 범위(자신과 서브 트리)와 구조 변경 이벤트 핸들러를 인자로 UI 자동화 이벤트를 등록합니다. 여기에서는 Automation 클래스의 AddStruectureChangedEventHandler 정적 메서드를 등록합니다. Automation.AddStructureChangedEventHandler( wae.AE,TreeScope.Element|TreeScope.Subtree, new StructureChangedEventHandler(OnStructureChanged)); 서브 트리의 자동화 요소를 검색하기 위해 활성화 속성이 참인 조건을 생성하여 검색합니다. Condition cond = new PropertyCondition(AutomationElement.IsEnabledProperty,true); AutomationElementCollection aec = wae.AE.FindAll(TreeScope.Subtree, cond); 조사한 각 항목을 래핑한 개체로 생성하고 구조 변경 이벤트 핸들러를 등록합니다. foreach(AutomationElement ae in aec) { WrapAE cwae = new WrapAE(ae); cwae.StructureChanged+=new StructureChangedEventHandler(OnStructureChanged); lbox_ae.Items.Insert(0,cwae); } } }
구조 변경 이벤트 핸드러를 작성합시다.
private void OnStructureChanged(object sender, StructureChangedEventArgs e) { 먼저 sender를 자동화 요소 개체를 참조합니다. AutomationElement ae = sender as AutomationElement; 그리고 래핑한 개체를 생성합니다. WrapAE wae = new WrapAE(ae); 구조 변경 이벤트 종류에 따라 이벤트 정보를 추가합니다. 이벤트 정보를 추가하는 기능은 별도의 메서드로 정의하여 사용합시다. switch (e.StructureChangeType) { case StructureChangeType.ChildAdded: AddStructChangeEvent("자식 추가",wae); break; case StructureChangeType.ChildRemoved: AddStructChangeEvent("자식 제거", wae); break; } 구조 변경 이벤트가 발생하면 자동화 요소 리스트 상자를 다시 설정합니다. SetAEList(); }
구조 변경 이벤트 정보를 표시하는 메서드도 크로스 스레드 문제가 발생할 수 있으므로 대리자 형식을 정의합니다.
delegate void MyEventDele(string eventmsg,WrapAE wae);
구조 변경 이벤트 정보를 표시하는 메서드를 구현합시다.
private void AddStructChangeEvent(string eventmsg, WrapAE wae) { 먼저 수행하는 스레드가 컨트롤을 소유하는 스레드인지 확인합니다. if (lbox_event.InvokeRequired) { 다른 스레드이면 컨트롤의 Invoke 메서드를 이용하여 소유 스레드가 대행할 수 있게 합니다. lbox_event.Invoke(new MyEventDele(AddStructChangeEvent), new object[] { eventmsg, wae }); } else { 같은 스레드면 이벤트 리스트 상자에 추가합니다. lbox_event.Items.Add(string.Format("{0}:{1}", wae,eventmsg)); } }
프로세스 리스트 상자에서 선택한 자동화 요소를 래핑한 개체를 반환하는 메서드를 작성하세요. 이 부분은 다른 실습에서도 계속 나온 부분이라 설명을 생략합니다.
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 Form1_FormClosing(object sender, FormClosingEventArgs e) { Automation.RemoveAllEventHandlers(); }
using System; using System.Windows.Forms; using System.Diagnostics; using System.Windows.Automation; namespace Ex_구조변경감시자 { public partial class Form1 : Form { public Form1(){ InitializeComponent(); } private void btn_refresh_Click(object sender, EventArgs e) { lbox_process.Items.Clear(); lbox_event.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) { SetAEList(); } delegate void MySetListDele(); private void SetAEList() { if (this.InvokeRequired) { try{ this.Invoke(new MySetListDele(SetAEList)); } catch{ } } else { lbox_ae.Items.Clear(); Automation.RemoveAllEventHandlers(); WrapAE wae = SelectedWrapAE(); if (wae == null) { return; } Automation.AddStructureChangedEventHandler( wae.AE, TreeScope.Element | TreeScope.Subtree, new StructureChangedEventHandler(OnStructureChanged)); Condition cond = new PropertyCondition( AutomationElement.IsEnabledProperty,true); AutomationElementCollection aec = wae.AE.FindAll(TreeScope.Subtree, cond); foreach(AutomationElement ae in aec) { WrapAE cwae = new WrapAE(ae); cwae.StructureChanged+=new StructureChangedEventHandler( OnStructureChanged); lbox_ae.Items.Insert(0,cwae); } } } private void OnStructureChanged(object sender, StructureChangedEventArgs e) { AutomationElement ae = sender as AutomationElement; WrapAE wae = new WrapAE(ae); switch (e.StructureChangeType) { case StructureChangeType.ChildAdded: AddStructChangeEvent("자식 추가",wae); break; case StructureChangeType.ChildRemoved: AddStructChangeEvent("자식 제거", wae); break; } SetAEList(); } delegate void MyEventDele(string eventmsg,WrapAE wae); private void AddStructChangeEvent(string eventmsg, WrapAE wae) { if (lbox_event.InvokeRequired) { lbox_event.Invoke(new MyEventDele(AddStructChangeEvent), new object[] { eventmsg, wae }); } else { lbox_event.Items.Add(string.Format("{0}:{1}", wae,eventmsg)); } } 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 Form1_FormClosing(object sender, FormClosingEventArgs e) { Automation.RemoveAllEventHandlers(); } } }
[소스 6.3] Form1.cs