5) InvokePattern, 6) 실습: InvokePattern 요소 제어하기

5) InvokePattern

InvokePattern은 버튼처럼 작업을 수행하도록 명령을 내릴 수 있는 컨트롤의 패턴입니다.

System.Object
System.Windows.Automation.BasePattern
System.Windows.Automation.InvokePattern

네임스페이스:  System.Windows.Automation
어셈블리:  UIAutomationClient(UIAutomationClient.dll)

InvokePattern 클래스에는 작업을 수행하게 명령하는 Invoke 메서드를 제공합니다. 그리고 정적 멤버 필드로 InvokedEvent를 제공하여 InvokePattern 컨트롤에 의해 작업을 수행하거나 활성화할 때 발생하는 이벤트를 식별할 수 있습니다.

6) 실습: InvokePattern 요소 제어하기

이번에는 InvokePattern 개체를 이용하여 다른 프로세스를 간단하게 제어하는 프로그램을 만들어 봅시다.

[그림 5.1] Invoke 요소 제어기
[그림 5.1] Invoke 요소 제어기

이번에 작성할 Invoke 요소 제어기는 Windows Forms 응용 프로그램입니다. 프로세스 목록 새로고침 버튼을 클릭하면 메인 창이 있는 프로세스 목록이 리스트 상자에 표시합니다. 그리고 프로세스 리스트 상자에서 항목을 선택하면 해당 프로세스의 메인 창의 자식 요소 중에 InvokePatten 을 지원하는 자동화 요소를 조사하여 가운데 리스트 상자에 표시합니다. 그리고 가운데 상자의 항목을 선택하면 선택한 항목과 매핑하는 컨트롤을 Invoke 합니다. 이는 다른 프로세스의 버튼을 클릭한 것과 같은 효과를 가져옵니다. 그리고 선택한 프로세스의 Main 창의 자식 컨트롤에 의해 Invoke 이벤트를 감지하면 마지막 리스트 상자에 표시하는 프로그램입니다.

앞으로 많은 곳에서 Process를 래핑하거나 자동화 요소를 래핑하는 등의 작업은 중복해서 발생합니다. 매 번 같은 작업을 하는 것은 효과적이지 않아 클래스 라이브러리를 만들어서 사용하기로 할게요. 앞으로 이 라이브러리는 추가 및 변경할 때에만 언급하기로 할게요.

먼저 WrapLib 이름으로 클래스 라이브러리를 추가하세요.

[그림 5.2] 클래스 라이브러리 생성
[그림 5.2] 클래스 라이브러리 생성

기본으로 제공하는 Class1.cs 파일의 속성을 WrapProcess로 변경하세요. WrapProcess 클래스는 앞에서 작성한 것과 차이가 없습니다. Visual Studio의 인텔리센스 기능을 활용하기 위해 세 줄 주석만 추가할게요.

using System;
using System.Diagnostics;
using System.Windows.Automation;
namespace WrapLib
{
    /// <summary>
    /// Process 개체를 래핑한 클래스
    /// </summary>
    public class WrapProcess
    {
        /// <summary>
        /// Process 개체 - 가져오기
        /// </summary>
        public Process Process{    get;    private set;    }
        /// <summary>
        /// 프로세스 이름과 메인 윈도우의 캡션 - 가져오기
        /// </summary>
        public string Title
        {
            get
            {
                return Process.ProcessName + ":" + Process.MainWindowTitle;
            }
        }
        /// <summary>
        /// 메인 윈도우와 매핑하는 자동화 요소 - 가져오기
        /// </summary>
        public AutomationElement RootElement
        {
            get
            {
                if (Process.MainWindowHandle == IntPtr.Zero)
                {
                    return null;
                }
                return AutomationElement.FromHandle(Process.MainWindowHandle);
            }
        }
        /// <summary>
        /// 생성자
        /// </summary>
        /// <param name="process">래핑할 프로세스 개체</param>
        public WrapProcess(Process process)
        {
            Process = process;
        }
        /// <summary>
        /// ToString 메서드 재정의
        /// </summary>
        /// <returns>Title 속성</returns>
        public override string ToString()
        {
            return Title;
        }
    }
}

[소스 5.1] WrapProcess.cs

여기에서는 InvokePattern 을 조사하여 사용할 것입니다. WrapLib 프로젝트에 WrapInvoke 클래스를 추가하세요.

public class WrapInvoke

WrapInvoke 클래스에는 자동화 요소를 래핑할 것이며 이를 참조할 수 있는 속성을 제공합시다.

public AutomationElement AE
{
    get;
    private set;
}

그리고 자동화 요소의 이름을 가져오기 할 수 있게 속성을 제공합시다.

public string Name
{
    get
    {
        return AE.Current.Name;
    }
}

자동화 요소의 지역화 컨트롤 타입명도 가져오기 할 수 있게 속성을 제공합시다.

public string ControlType
{
    get
    {
        return AE.Current.LocalizedControlType;
    }
}

생성자는 입력 인자로 래핑할 자동화 요소를 전달받습니다. 그리고 전달받은 자동화 요소를 AE 속성에 설정합니다.

public WrapInvoke(AutomationElement ae)
{
    AE = ae;
}

InvokePattern의 Invoke 메서드를 래핑한 메서드를 제공합시다. 메서드 내부에서는 래핑한 자동화 요소 개체가 InvokePattern 개체로 참조한 후에 Invoke 메서드를 호출합니다.

public void Invoke()
{
    try
    {
        InvokePattern inv_pat = AE.GetCurrentPattern(InvokePattern.Pattern) as InvokePattern;
        inv_pat.Invoke();
    }
    catch { }

}

ToString 메서드도 재정의합시다.

public override string ToString()
{
    return ControlType + ":" + Name;
}
using System.Windows.Automation;
namespace WrapLib
{
    /// <summary>
    /// InvokePattern의 자동화 요소 래핑 클래스
    /// </summary>
    public class WrapInvoke
    {
        /// <summary>
        /// 자동화 요소 - 가져오기
        /// </summary>
        public AutomationElement AE{    get;    private set;    }
        /// <summary>
        /// 자동화 요소의 이름 - 가져오기
        /// </summary>
        public string Name
        {
            get
            {
                return AE.Current.Name;
            }
        }
        /// <summary>
        /// 자동화 요소의 지역화 컨트롤 타입명 - 가져오기
        /// </summary>
        public string ControlType
        {
            get
            {
                return AE.Current.LocalizedControlType;
            }
        }
        /// <summary>
        /// 생성자
        /// </summary>
        /// <param name="ae">래핑할 자동화 요소</param>
        public WrapInvoke(AutomationElement ae)
        {
            AE = ae;
        }
        /// <summary>
        /// 래핑한 InvokePattern의 Invoke 메서드 호출
        /// </summary>
        public void Invoke()
        {
            try
            {
                InvokePattern inv_pat = AE.GetCurrentPattern(
                                       InvokePattern.Pattern) as InvokePattern;
                inv_pat.Invoke();
            }
            catch { }
        }
        /// <summary>
        /// ToString 메서드 재정의
        /// </summary>
        /// <returns>ControlType 속성과 Name 속성을 합한 문자열</returns>
        public override string ToString()
        {
            return ControlType + ":" + Name;
        }
    }
}

[소스 5.2] WrapInvoke.cs

작성한 클래스 라이브러리를 참조 추가할 때는 솔루션 창의 프로젝트의 참조에서 컨텍스트 메뉴를 통해 참조 추가할 수 있습니다.

[그림 5.3] 어셈블리 참조 추가
[그림 5.3] 어셈블리 참조 추가

이제 Invoke 요소 제어기를 작성합니다. 먼저 프로젝트 추가를 통해 Windows Forms 응용 프로그램을 선택하세요.

물론 WrapLib를 참조 추가 및 UI 자동화 기술에 관한 어셈블리도 참조 추가해야 합니다.

이제 메인 폼의 컨트롤을 배치합시다.

[그림 5.4] Main 폼 컨트롤 배치
[그림 5.4] Main 폼 컨트롤 배치

Main 폼에는 버튼 1개와 ListBox 3개를 배치합니다.

번호컨트롤 타입컨트롤 명설명
1Buttonbtn_refresh프로세스 목록 새로고침
2ListBoxlbox_process프로세스 목록을 보여주는 리스트 상자
3ListBoxlbox_inv_elem선택한 프로세스의 InvokePattern 자동화 요소 목록을 보여주는 리스트 상자
4ListBoxlbox_history선택한 프로세스의 InvokeEvent를 보여주는 리스트 상자

[표 5.8] Form1의 컨트롤

먼저 프로세스 목록 새로고침 버튼의 클릭 이벤트 핸드러를 추가합니다.

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)
{
 InvokePattern 리스트 상자의 항목을 지웁니다.
    lbox_inv_elem.Items.Clear();
 프로세스 리스트 상자의 선택 항목에서 래팽한 InvokePattern 자동화 요소 개체를 얻어옵니다. 이 부분은 별도의 메서드로 정의합시다.
    WrapInvoke wae = SelectedWrapAE();
 반환 값이 null이면 이벤트 핸들러를 종료합니다.
    if (wae == null)
    {
        return;
    }
 Automation 클래스의 정적 메서드 AddAutomationEventHandler를 호출하여 InvokePattern에 Invoke 이벤트가 발생할 때 처리할 이벤트 핸드러를 등록합니다.
    Automation.AddAutomationEventHandler(InvokePattern.InvokedEvent,
                     wae.AE, TreeScope.Subtree,
                     MyEventHandler = new AutomationEventHandler(OnUIAutomationEvent));
 그리고 InvokePattern인 개체를 찾기 위한 Condition 개체를 생성합니다.
    Condition cond = new PropertyCondition(
                     AutomationElement.IsInvokePatternAvailableProperty, true);
 조건 개체와 탐색할 조건을 서브 트리인 인자를 전달하여 조건에 만족하는 자동화 요소 컬렉션을 구합니다.
    AutomationElementCollection aec = wae.AE.FindAll(TreeScope.Subtree, cond);
 자동화 요소 컬렉션에 있는 자동화 요소 개체를 WrapInvoke 개체로 감싼 후에 InvokePattern 개체를 표시할 리스트 상자 항목에 추가합니다.
    foreach (AutomationElement ae in aec)
    {
        lbox_inv_elem.Items.Add(new WrapInvoke(ae));
    }
}
 프로세스 리스트 상자에서 선택한 항목을 WrapInvoke 개체로 래핑하여 반환하는 메서드를 정의합시다.
private WrapInvoke SelectedWrapAE()
{
 만약 선택항목이 없으면 null을 반환합니다.
    if (lbox_process.SelectedIndex == -1)
    {
        return null;
    }
 선택항목을 WrapProcess 형식으로 참조합니다.
    WrapProcess wp = lbox_process.SelectedItem as WrapProcess;
 프로세스의 Root 자동화 요소를 얻어와서 WrapInvoke 개체를 생성하여 반환합니다.
    AutomationElement ae = wp.RootElement;
    return new WrapInvoke(ae);
}
 Invoke 관련 이벤트를 감지하였을 때 처리할 이벤트 핸들러를 정의합시다.
private void OnUIAutomationEvent(object src, AutomationEventArgs e)
{
 src로 전달받은 개체를 자동화 요소 개체로 참조합니다.
    AutomationElement ae = src as AutomationElement;
 이벤트를 리스트 상자에 추가하는 메서드를 호출합시다.
    AddEvent(ae);
}

리스트 상자에 추가하는 메서드를 정의합시다. 그런데 Invoke 관련 이벤트를 감지하여 전달하는 스레드와 리스트 상자를 소유하는 스레드가 다르기 때문에 크로스 스레드 문제가 발생합니다. 이를 위해 AddEvent 메서드와 반환 형식과 입력 인자 리스트가 같은 대리자를 정의합시다.

delegate void MyDele(AutomationElement ae);
private void AddEvent(AutomationElement ae)
{
 만약 리스트 상자의 InvokeRequired 속성이 참이면 현재 수행하는 스레드와 리스트 상자의 스레드가 서로 다름을 의미합니다. 이 때는 인자를 object 배열 개체로 생성하고 대리자 개체를 생성하여 리스트 상자의 Invoke 메서드를 호출합니다. 이는 Windows Forms 응용 프로그램의 어떠한 컨트롤에서도 같은 방법으로 크로스 스레드 문제를 해결할 수 있습니다.
    if (lbox_history.InvokeRequired)
    {
        object[] objs = new object[1] { ae };
        lbox_history.Invoke(new MyDele(AddEvent), objs);
    }
    else
    {
        lbox_history.Items.Add(ae.Current.Name+" 클릭");
    }
}

InvokePattern 개체를 항목으로 표시한 리스트 상자의 선택 변경 이벤트 핸들러를 추가하세요.

private void lbox_inv_elem_SelectedIndexChanged(object sender, EventArgs e)
{
 선택 항목이 없으면 이벤트 핸들러를 종료합니다.
    if (lbox_inv_elem.SelectedIndex == -1){    return;    }
 선택항목을 WrapInvoke 형식으로 참조하여 Invoke 메서드를 호출합니다.
    WrapInvoke winvoke = lbox_inv_elem.SelectedItem as WrapInvoke;
    winvoke.Invoke();
}

이제 프로그램을 빌드하여 테스트를 해 보세요.

using System;
using System.Windows.Forms;
using System.Diagnostics;
using System.Windows.Automation;
using WrapLib;

namespace Invoke요소_제어기
{
    public partial class MainForm : Form
    {
        AutomationEventHandler MyEventHandler;

        public MainForm()
        {
            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)
        {
            lbox_inv_elem.Items.Clear();
            WrapInvoke wae = SelectedWrapAE();
            if (wae == null)
            {
                return;
            }            
            Automation.AddAutomationEventHandler(InvokePattern.InvokedEvent,
                     wae.AE, TreeScope.Subtree,
                     MyEventHandler = new AutomationEventHandler(OnUIAutomationEvent));

            Condition cond = new PropertyCondition(
                       AutomationElement.IsInvokePatternAvailableProperty, true);
            AutomationElementCollection aec = wae.AE.FindAll(TreeScope.Subtree, cond);
            foreach (AutomationElement ae in aec)
            {
                lbox_inv_elem.Items.Add(new WrapInvoke(ae));
            }
        }
        private WrapInvoke SelectedWrapAE()
        {
            if (lbox_process.SelectedIndex == -1)
            {
                return null;
            }

            WrapProcess wp = lbox_process.SelectedItem as WrapProcess;
            AutomationElement ae = wp.RootElement;
            return new WrapInvoke(ae);
        }
        private void OnUIAutomationEvent(object src, AutomationEventArgs e)
        {
            AutomationElement ae = src as AutomationElement;
            AddEvent(ae);
        }

        delegate void MyDele(AutomationElement ae);
        private void AddEvent(AutomationElement ae)
        {
            if (lbox_history.InvokeRequired)
            {
                object[] objs = new object[1] { ae };
                lbox_history.Invoke(new MyDele(AddEvent), objs);
            }
            else
            {
                lbox_history.Items.Add(ae.Current.Name+" 클릭");
            }
        }

        private void lbox_inv_elem_SelectedIndexChanged(object sender, EventArgs e)
        {
            if (lbox_inv_elem.SelectedIndex == -1)
            {
                return;
            }
            WrapInvoke winvoke = lbox_inv_elem.SelectedItem as WrapInvoke;
            winvoke.Invoke();
        }
    }
}

[소스 5.3] Form1.cs