프로그래밍 기술/소프트웨어 접근성, UI 자동화

9. 접근성 평가 도구 만들기 - 10. AccEvalProject 클래스 구현

언제나휴일 2016. 10. 25. 17:45
반응형

9.2.7 AccEvalProject 클래스

 

 접근성 평가 프로젝트의 주요 정보를 수집하고 이를 관리하는 클래스를 정의합시다. 클래스의 이름은 AccEvalProject라고 정할게요.

 

 AccEvalProject에서는 자동화 요소를 수집하여 계층적인 형태로 보관하는 부분이 있습니다. 그리고 폼에서 특정 노드를 선택하면 해당 노드와 매핑하는 자동화 요소를 찾을 있어야 하는데 여기서는 자동화 요소를 구분할 있는 문자열과 트리 노드를 쌍으로 하는 사전을 멤버로 선언할게요.

Dictionary<stringTreeNode> node_dic = null;

 

 그리고 평가 프로젝트를 종료할 때 이 사실을 통보하기 위한 이벤트와 평가 대상 창의 사각 영역이 변경되었을 때 통보하기 위한 이벤트를 선언할게요.

 

public event EventHandler EndEvalProject = null;

public event AutomationPropertyChangedEventHandler AEMoved = null;

 

 평가 대상 창이 닫히거나 대상 창의 사각 영역이 변경되는 것은 자동화 이벤트를 이용합니다. 따라서 평가 프로젝트를 종료할 때 등록한 자동화 이벤트를 해제하기 위해 이벤트 핸들러를 멤버 필드로 선언하여 기억하고 있어야 합니다.

AutomationPropertyChangedEventHandler apceh = null;

AutomationEventHandler close_eventHandler = null;

 

 자동화 이벤트를 등록하고 해제할 자동화 요소를 알아야 하므로 평가 대상 창의 자동화 요소를 멤버 필드로 기억하게 합시다.

AutomationElement mae = null;

 

 

 AccEvalProject 클래스에 속성으로 평가 제목, 자동화 요소의 전체 개수, 평가 테이블, 평가 대상 프로세스, 대상 프로세스의 Main Window Handle, Root 노드, 이미지를 제공하고 문자열로 TreeNode 선택할 있는 컬렉션을 제공합시다.

public string Title{    get;    private set;    }

public int UICount

{

    get

    {

        return Table.Rows.Count;

    }

}

public DataTable Table{    get;   private set;    }

public EHProcess EHProcess{    get;    private set;    }

public IntPtr MainWndHandle

{

    get

    {

        return EHProcess.MainHandle;

    }

}

public TreeNode Root{    get;    private set;    }

public Image Image{   get;   private set;    }

public TreeNode this[string key]

{

    get

    {

        return node_dic[key];

    }

}

 

 

 

 

 

 

 

 생성자는 프로젝트 제목과 DataTable, 프로세스 개체를 인자로 받아 속성에 설정하고 문자열을 키로하고 트리 노드를 값으로 하는 사전을 생성합니다. 여기서 문자열은 자동화 요소를 구분하기 위한 문자열이고 트리 노드는 자동화 요소를 계층적으로 관리하는 요소와 매핑한 트리 노드입니다.

public AccEvalProject(string title, DataTable dt, EHProcess ehprocess)

{

    Title = title;

    Table = dt;

    EHProcess = ehprocess;

    node_dic = new Dictionary<stringTreeNode>();

}

 

 프로세스를 초기화하는 메서드를 제공하여 메인 창의 사각 영역이 변하는 것과 메인 창이 닫힘을 통보받는 이벤트 핸들러를 등록합니다. 그리고 메인 창의 계층적 구조와 서브 트리의 요소들을 찾습니다. 메서드에서는 발견한 자동화 요소의 개수를 입력받아 처리하는 대리자를 입력 인자로 받습니다.

public void InitProcessMode(AddFindElementDele find_dele)

{

    mae = AutomationElement.FromHandle(MainWndHandle);

    apceh = new AutomationPropertyChangedEventHandler(OnPropertyChange);

    Automation.AddAutomationPropertyChangedEventHandler(mae,

        TreeScope.Element, apceh, AutomationElement.BoundingRectangleProperty);

    close_eventHandler =   new AutomationEventHandler(OnWindowClose);

    Automation.AddAutomationEventHandler(WindowPattern.WindowClosedEvent,

        mae, TreeScope.Element, close_eventHandler);

    FindAETree(mae,find_dele);

}

 

 

 메인 창의 사각 영역이 변경됨을 통보 받는 이벤트 핸들러에서는 필요한 곳에 전달합니다. 만약 보다 강력한 평가 도구를 만들려면 다른 속성 변경에 대한 부분도 처리하세요.

private void OnPropertyChange(object src, AutomationPropertyChangedEventArgs e)

{

    AutomationElement sourceElement = src as AutomationElement;

    if (e.Property == AutomationElement.BoundingRectangleProperty)

    {

        if (AEMoved != null)

        {

            AEMoved(this, e);

        }

    }

}

 

 메인 창이 닫힘을 통보 받는 이벤트 핸들러에서는 프로젝트를 끝내는 메서드를 호출합니다.

private void OnWindowClose(object src, AutomationEventArgs e)

{

    AutomationElement ae;

    try

    {

        ae = src as AutomationElement;

    }

    catch

    {

        return;

    }

    End();

}

 

 

 

 

 

 

 

 메인 창의 자식을 포함한 자동화 요소를 찾는 메서드를 구현합시다.

private void FindAETree(AutomationElement ae,AddFindElementDele find_dele)

{

}

 

 FindAETree 메서드에서는 프로젝트의 메인 창에 초점을 주어 Z좌표 상단에 배치하도록 합니다. 일부 프로그램에서 메인 창이 초점을 갖지 못할 수 있으므로 서브 트리의 요소 중에 초점을 갖을 수 있는 요소를 찾아 초점을 갖게 해 줍니다.

try

{

    ae.SetFocus();

    Thread.Sleep(100);

}

catch

{

    try

    {

        AutomationElement sae = ae.FindFirst(TreeScope.Subtree ,

                new PropertyCondition(

                         AutomationElement.IsKeyboardFocusableProperty, true));

        sae.SetFocus();

        Thread.Sleep(100);

    }

    catch

    {

        MessageBox.Show("선택한 윈도우에 초점을 가질  있는 요소가 없습니다.");

    }

}

 

 또한 자동화 요소의 화면 이미지를 캡쳐하여 이미지 속성에 설정합니다.

try

{

    Image = ImageCapture.CaptureFormRect(ae.Current.BoundingRectangle);

}

catch{   }

 

 그리고 메인 윈도 창의 자동화 요소를 발견한 것이므로 입력 인자로 받은 대리자를 호출하여 인자 1을 전달합니다.

if (find_dele != null)

{

    find_dele(1);

}

 

 자동화 요소를 래핑한 EHAutoElem 개체를 생성한 후에 자동화 요소를 구조적으로 분석하여 트리 노드를 만드는 메서드를 호출합니다.

EHAutoElem eae = new EHAutoElem(ae);

Root = MakeUIHierarchy(eae, find_dele);

 

 MakeUIHierarchy 메서드를 구현합시다.

TreeNode MakeUIHierarchy(EHAutoElem eae, AddFindElementDele find_dele){  }

 

입력 인자로 들어온 요소로 트리 노드를 생성합니다.

TreeNode sr = new TreeNode(eae.ToString());

 

 요소를 구분할 문자열을 얻어와서 사전에 이미 있는 요소인지 확인합니다. 없을 때는 사전에 추가하고 노드의 태그에는 요소를 설정하고 요소의 태그에는 트리 노드를 설정합니다. 이는 어떤 방향으로든 매핑 상대를 알 수 있게 하기 위해서입니다. 그리고 검색한 요소 정보를 처리하는 메서드를 호출합니다.

string ehid = eae.GetEHID();

if (node_dic.ContainsKey(ehid) == false)

{

    node_dic[ehid] = sr;

    sr.Tag = eae;

    eae.Tag = sr;

    FindedAutoElem(nullnew FindAutoElemEventArgs(eae));

}

else

{

    return null;

}

 

 

 현재 요소의 자식 요소를 검색하고 개수를 입력 인자로 받은 대리자로 전달합니다.

Condition con = TreeWalker.RawViewWalker.Condition;

AutomationElement ae = eae.AE;

AutomationElementCollection aec = ae.FindAll(TreeScope.Children, con);

if (find_dele != null)

{

    find_dele(aec.Count);

}

 

 그리고 검색한 자식 요소를 입력 인자로 MakeUIHierarchy 함수를 재귀적으로 호출합니다. 물론 재귀 호출의 결과를 현재의 트리 노드의 자식 노드로 추가합니다. 재귀 호출의 결과가 null이면 이미 발견한 요소이므로 null 이 아닐 때만 처리합니다.

foreach (AutomationElement sae in aec)

{

    TreeNode tr = null;

    if ((tr = MakeUIHierarchy(new EHAutoElem(sae), find_dele))!= null)

    {

        sr.Nodes.Add(tr);

    }

}

 

 마지막으로 sr을 반환합니다.

 

 

 새로 발견한 자동화 요소를 처리하는 FindedAutoElement 메서드를 구현합시다. 여기서는 테이블의 새로운 행을 하나 생성하여 자동화 속성과 컨트롤 패턴을 얻어와 행의 셀에 추가합니다. 그리고 테이블에 추가하세요.

void FindedAutoElement(object send, FindAutoElemEventArgs e)

{

    EHAutoElem eae = e.EAE;

    if (Table == null){    return;    }

    DataRow dr = Table.NewRow();

    for (ENUM_UIProperty uip = 0; uip < ENUM_UIProperty.MAX_UIPROPERTY; uip++)

    {

        dr[uip.ToString()] = eae.GetAEProperty(uip);

    }

    for (ENUM_CONTROL ctrl = 0; ctrl < ENUM_CONTROL.MAX_CONTROL; ctrl++)

    {

        dr[ctrl.ToString()] = eae.GetPattern(ctrl);

    }

    dr["EHAutoElem"] = eae;

    Table.Rows.Add(dr);

}

 

 프로젝트를 마치는 메서드에서는 이를 통보받기 원하는 이벤트 핸들러가 있을 이벤트 통보를 하는 작업과 등록한 자동화 이벤트 핸들러를 해제하는 작업을 수행합니다.

public void End()

{

    if (EndEvalProject != null)

    {

        EndEvalProject(thisnew EventArgs());

    }

    Automation.RemoveAutomationPropertyChangedEventHandler(mae, apceh);

    Automation.RemoveAutomationEventHandler(

             WindowPattern.WindowClosedEvent, mae, close_eventHandler);

}

 

 

 

 

 평가 결과를 저장하는 메서드를 제공합시다. 입력 인자로 평가 결과를 저장할 폴더를 받습니다. 만약 해당 위치에 평가 결과가 있으면 번호를 붙여 평가 결과를 저장합니다. 저장할 평가 결과는 요소의 정보를 문서로 저장하는 것과 이미지를 저장하는 것이 있습니다.

public void Save(string path)

{

    string dirname = path + "/" + Table.TableName;

    if (Directory.Exists(dirname))

    {

        int i = 1;

        dirname = path + "/" + Table.TableName + "_" + i.ToString();

        while (Directory.Exists(dirname))

        {

            i++;

           dirname = path + "/" + Table.TableName +"_" + i.ToString();

        }

    }

    Directory.CreateDirectory(dirname);

    SaveReport(dirname);

    SaveImageAll(dirname);

}

 

 평가 결과를 문서로 저장하는 메서드에서는 평가 제목, 전체 요소 개수, 이름 없는 요소 개수, 컨트롤 패턴 별 개수, Accelerator 키 목록, Access 키 목록, 자동화 요소 정보를 저장합니다.

public void SaveReport(string dir)

{

    string cdir = Directory.GetCurrentDirectory();

    Directory.SetCurrentDirectory(dir);

    XmlWriterSettings settings = new XmlWriterSettings();

    settings.OmitXmlDeclaration = false;

    settings.CheckCharacters = false;

    settings.Indent = true;

    settings.Encoding = System.Text.Encoding.Default;

    XmlWriter writer = XmlWriter.Create(dir + "/report.xml",settings);

 

 

    writer.WriteStartElement("Report");

    writer.WriteElementString("Title", Title);

    writer.WriteElementString("전체_개수", UICount.ToString());

    writer.WriteElementString("이름없는_UI_요소_개수", NonameCount.ToString());

    WriteControlCount(writer);

    WriteAcceleratorKey(writer);

    WriteAccessKey(writer);

    WriteUIElement(writer);

    writer.WriteEndElement();

    writer.Close();

    Directory.SetCurrentDirectory(cdir);

}

 

 이름 없는 UI 요소 개수는 테이블의 각 행의 이름 셀의 내용을 검토하여 빈 문자열의 개수를 세어 반환합니다.

public int NonameCount

{

    get

    {

        int count = 0;

        foreach (DataRow dr in Table.Rows)

        {

            if (dr[(int)ENUM_UIProperty.NAME].ToString() == string.Empty)

            {

                count++;

            }

        }

        return count;

    }

}

 

 

 Access 키와 Accelerator 키 목록을 저장하는 메서드에서는 먼저 목록을 구하는 메서드를 호출하여 목록의 정보들을 요소로 저장합니다. 키로 사용할 문자열에 공백, 괄호 등의 문자는 빼야 XML 요소 이름으로 사용할 수 있습니다. 여기서는 XML 요소 이름으로 포함할 수 없는 일부 문자를 제거하게 작성하였습니다. 여러분은 추가로 XML 요소로 사용할 수 없는 문자를 제거하게 작성해서 품질 수준을 높여보세요.

private void WriteAccessKey(XmlWriter writer)

{

    writer.WriteStartElement("Access");

    List<string[]> keylist = new List<string[]>();

    keylist = GetAccessKeyItemList();

    foreach (string[] key_value in keylist)

    {

       writer.WriteElementString(ConvertString(key_value[0]),ConvertString(key_value[1]));

    }

    writer.WriteEndElement();

}

public List<string[]> GetAcceleratorKeyItemList()

{

    List<string[]> item_list = new List<string[]>();

    DataTable dt = Table;

    EHAutoElem eae = null;

    foreach (DataRow dr in dt.Rows)

    {

        eae = dr["EHAutoElem"as EHAutoElem;

        if (eae.AE.Current.AcceleratorKey != string.Empty)

        {

            string[] name_key = new string[2];

            name_key[0] = eae.ToString();

            name_key[1] = eae.AE.Current.AcceleratorKey;

            item_list.Add(name_key);

        }

    }

    return item_list;

}

 

 

private void WriteAcceleratorKey(XmlWriter writer)

{

    writer.WriteStartElement("Accelerator");

    List<string[]> keylist = new List<string[]>();

    keylist = GetAcceleratorKeyItemList();

    foreach (string[] key_value in keylist)

    {

        writer.WriteElementString(ConvertString(key_value[0]), key_value[1]);

    }

    writer.WriteEndElement();

}

public List<string[]> GetAccessKeyItemList()

{

    List<string[]> item_list = new List<string[]>();

    DataTable dt = Table;

    EHAutoElem eae = null;

    foreach (DataRow dr in dt.Rows)

    {

        eae = dr["EHAutoElem"as EHAutoElem;

        if (eae.AE.Current.AccessKey != string.Empty)

        {

            string[] name_key = new string[2];

            name_key[0] = eae.ToString();

            name_key[1] = eae.AE.Current.AccessKey;

            item_list.Add(name_key);

        }

    }

    return item_list;

}

private string ConvertString(string str)

{

    char[] trim_chars = new char[] { ' '')''('':''+' };

    return str.Trim(trim_chars);

}

 

 

 컨트롤 유형 별 요소 개수를 저장하는 메서드에서는 컨트롤 유형별 개수를 얻어오는 메서드를 호출하여 이 정보를 출력합니다. 컨트롤 유형별 개수는 평가 테이블에 저장하였는데 개수가 없는 셀은 빈 상태입니다. 테이블의 특정 셀이 비어있는지 확인할 때는 null과 비교하지 않고 DBNull.Value와 비교합니다.

private void WriteControlCount(XmlWriter writer)

{

    writer.WriteStartElement("컨트롤_유형__개수");

    for (ENUM_CONTROL ctrl = 0; ctrl < ENUM_CONTROL.MAX_CONTROL; ctrl++)

    {

        int count = GetControlPatternCount(ctrl);

        writer.WriteElementString(ctrl.ToString(), count.ToString());

    }

    writer.WriteEndElement();

}

public int GetControlPatternCount(ENUM_CONTROL ctrl)

{

    int count = 0;

    DataTable dt = Table;

    foreach (DataRow dr in dt.Rows)

    {

        if (dr[ctrl.ToString()] != DBNull.Value)

        {

            count++;

        }

    }

    return count;

}

 

 

 

 자동화 요소의 속성정보를 저장하는 메서드도 같은 방법으로 구현합니다.

private void WriteUIElement(XmlWriter writer)

{

    writer.WriteStartElement("UI_요소_목록");

    foreach (DataRow dr in Table.Rows)

    {

        WriteUIInfo(writer, dr);

    }

    writer.WriteEndElement();

}

private void WriteUIInfo(XmlWriter writer, DataRow dr)

{

    writer.WriteStartElement("UI요소");

    for (ENUM_UIProperty eup=0; eup < ENUM_UIProperty.MAX_UIPROPERTY; eup++)

    {

        string pname = eup.ToString();

        string value = ConvertString(dr[eup.ToString()].ToString());

        writer.WriteElementString(pname,value);

    }

    writer.WriteEndElement();

}

 

 이미지를 저장하는 메서드에서는 원본 이미지와 흑백 이미지를 저장하는 메서드를 만들어서 이를 호출하게 합시다.

public void SaveImageAll(string dir)

{

    SaveImage(dir);

    SaveGrayImage(dir);

}

 

 원본 이미지를 저장하는 메서드는 자동화 요소를 래핑한 EHAutoElem에 이미 캡쳐한 이미지가 있으니 이를 이용하여 저장합니다. 흑백 이미지도 마찬가지입니다.

 

 

 

 

public void SaveImage(string dir)

{

    string cdir = Directory.GetCurrentDirectory();

    Directory.SetCurrentDirectory(dir);

    Directory.CreateDirectory("이미지");

    int fname_index = 1;

    foreach (TreeNode tn in node_dic.Values)

    {

        EHAutoElem eae = tn.Tag as EHAutoElem;

        if (eae.Image != null)

        {

            eae.Image.Save("이미지/"+fname_index.ToString() + ".bmp");

        }

        fname_index++;

    }

    Directory.SetCurrentDirectory(cdir);

}

public void SaveGrayImage(string dir)

{

    string cdir = Directory.GetCurrentDirectory();

    Directory.SetCurrentDirectory(dir);

    Directory.CreateDirectory("흑백이미지");

    int fname_index = 1;

    foreach (TreeNode tn in node_dic.Values)

    {

        EHAutoElem eae = tn.Tag as EHAutoElem;

        if (eae.Image != null)

        {

            eae.GrayImage.Save("흑백이미지/" + fname_index.ToString() + ".bmp");

        }

        fname_index++;

    }

    Directory.SetCurrentDirectory(cdir);

}

 

반응형