NGMsoftware

NGMsoftware
로그인 회원가입
  • 매뉴얼
  • 팁 앤 테크
  • 매뉴얼

    팁과 테크니컬 노하우를 확인하세요.

    팁 앤 테크

    팁과 테크니컬 노하우를 확인하세요.

    본 사이트의 컨텐츠는 저작권법의 보호를 받으므로 무단 복사, 게재, 배포 등을 금합니다.

    커스텀 엔지엠 RPA 매크로 - 디자이너 또는 커스텀 모듈로 비활성 클릭 모듈 만들기. (Create an inactive click…

    페이지 정보

    본문

    안녕하세요. 엔지엠소프트웨어입니다. 오늘은 디자이너/프레임워크 또는 커스텀 모듈을 사용해서 비활성 마우스 클릭에 대해 알아보겠습니다. 우선, 활성/비활성/하드웨어 동작 방식에 대한 이해가 필요합니다. 제가 제공해드리는 코드를 복사/붙여넣기로 간단한 동작은 할겁니다. 하지만, 이 코드를 응용해서 뭔가 더 고차원적인 액션을 만들려면 기본적인 프로세스 로직을 이해하는게 중요합니다.

    [ 활성/비활성/하드웨어 방식으로 동작 설명 ]

     

    비활성은 마우스를 클릭하는 시점에 제어하는 컨트롤의 정보를 내부에 저장 해둡니다. 그리고, 동작 시점에 현재 정보와 비교합니다. 단순히 이전에 창 위치가 100, 100이었다고 가정합시다. 오늘 스크립트를 다시 실행할 때 창이 200, 200에 위치한다면 좌표값이 달라지게 되겠죠? 물론, 좌표값을 다시 만들면 되지만 이렇게하면 자동화를 사용하는 의미가 없게됩니다. 그래서, 이전의 창 위치와 현재의 창 위치를 비교해서 마우스 좌표값을 다이나믹하게 보정해줘야 합니다. 이렇게만 설명하면 간단해 보이지만, 막상 코드로 구현하려면 상당히 어려운 작업입니다. 이런 방식의 처리가 어렵기 때문에 대부분 모바일용 매크로들이 ADB를 사용하고 있습니다. 그래서 범용성이 떨어지고 안드로이드 OS에 종속적입니다. Visual Studio Community를 실행하고, 새로운 프로젝트를 추가하세요. 이름은 "CustomInactiveClick"으로 생성합니다. 물론, 프로젝트 형식은 "클래스 라이브러리"로 선택해야 합니다^^;

    wH0bt41.png

     

     

    엔지엠 엔터프라이즈 라이센스를 사용중이시면 아래와 같이 커스텀 모듈 개발용 라이브러리를 3개 추가해줘야 합니다.

    N5rLYiE.png

     

     

    도구 상자에 추가할 마우스 클릭 모델이기 때문에 "BaseCustomToolModel" 추상 클래스를 상속 받습니다. 그리고, 핸들을 구현하기 위해 "IHandleModel" 인터페이스도 상속 받습니다. IHandleModel 인터페이스를 구현하면 엔지엠의 매크로 엔진에서 핸들로 처리되는 많은 것들을 자동화할 수 있습니다. 마우스 클릭이 아니더라도 핸들로 뭔가 처리하고 싶은 액션이 있으면 IHandleModel 인터페이스를 상속 받고 구현해주면 됩니다.

    using NGM.Models.Item;
    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Drawing;
    using System.Drawing.Design;
    using System.Linq;
    using System.Xml.Serialization;
    
    namespace CustomInactiveClick
    {
        [Serializable]
        public class InactiveClickModel : NGM.Models.Interface.BaseCustomToolModel, NGM.Models.Interface.IHandleModel
        {
            private string _findControl;
    
            public override string DisplayCategory => "NGMsoftware";
    
            public override string DisplayName => "비활성 마우스 클릭";

     

     

    IHandleModel 인터페이스 구현부입니다. MainRectangle과 ControlRectangle은 핸들을 설정할 때 초기화됩니다. 이 값을 기준으로 창 위치가 변경되더라도 현재 창 위치와 비교해서 클릭할 위치를 계산할 수 있습니다. 절대 위치의 마우스 좌표를 아래 공식에 따라 상대 위치로 계산합니다.

    이전 위치 - 현재 위치 + 마우스 좌표 = 상대 좌표

            private Rectangle MainRectangle { get; set; }
    
            private Rectangle ControlRectangle { get; set; }
    
            [XmlIgnore]
            [Category("Data")]
            [DisplayName("매인 핸들")]
            [Description("컨트롤의 매인 핸들 값입니다.")]
            [Browsable(true)]
            [ReadOnly(true)]
            public IntPtr MainHandle { get; set; }
    
            [XmlIgnore]
            [Category("Data")]
            [DisplayName("컨트롤 핸들")]
            [Description("선택된 컨트롤의 핸들 값입니다.")]
            [Browsable(true)]
            [ReadOnly(true)]
            public IntPtr ControlHandle { get; set; }
    
            [Category("Action")]
            [DisplayName("컨트롤 제목")]
            [Description("선택한 컨트롤의 상단에 표시되는 제목입니다.")]
            [Browsable(true)]
            [DefaultValue(null)]
            public string Title { get; set; }
    
            [Category("Action")]
            [DisplayName("컨트롤 구조")]
            [Description("선택된 컨트롤의 구조입니다. 컨트롤을 찾기 위해 직접 편집이 가능합니다.")]
            [Browsable(true)]
            public List<NGM.Models.Item.HandleItem> Children { get; set; } = new List<NGM.Models.Item.HandleItem>();

     

     

    이 속성은 제어할 윈도우 또는 컨트롤의 핸들 값을 가져오는 프로세스입니다. "NGM.Models.TypeEditor.FindControlEditor" 특성을 사용하세요. 그러면 엔지엠 에디터에서 [ 핸들 추가 ] 액션과 동일한 기능을 추가할 수 있습니다. 아래 코드는 상당히 복잡해 보이는데요. 전체적인 내용은 간단합니다. 엔지엠 프레임워크에서 제공해주는 함수를 사용해서 제어할 윈도우의 매인 핸들과 컨트롤 핸들을 가져옵니다. 핸들 값을 알면 창의 위치도 알 수 있게 되는데요. Window 클래스의 GetWindowRect 함수를 사용합니다.

            [Category("Action")]
            [DisplayName("컨트롤 찾기")]
            [Description("응용 프로그램 또는 응용 프로그램의 컨트롤을 선택합니다.")]
            [Browsable(true)]
            [DefaultValue(null)]
            [Editor(typeof(NGM.Models.TypeEditor.FindControlEditor), typeof(System.Drawing.Design.UITypeEditor))]
            public virtual string FindControl
            {
                get { return _findControl; }
                set
                {
                    _findControl = value;
                    if (string.IsNullOrEmpty(_findControl))
                    {
                        this.Title = null;
                        this.MainHandle = IntPtr.Zero;
                        this.ControlHandle = IntPtr.Zero;
                        Children.Clear();
    
                        if (MainView != null && MainView is NGM.Interface.IEditor)
                        {
                            (MainView as NGM.Interface.IEditor).RefreshProperty();
                        }
    
                        return;
                    }
    
                    List<string> nodes;
                    nodes = _findControl.Split(new string[] { NGM.Definition.DOMSeparator }, StringSplitOptions.RemoveEmptyEntries).ToList();
                    Title = nodes.FirstOrDefault();
                    nodes.RemoveAt(0);
                    Children.Clear();
    
                    foreach (string node in nodes)
                    {
                        Children.Add(new HandleItem()
                        {
                            Node = (NGM.API.Options.FindWindow)Enum.Parse(typeof(NGM.API.Options.FindWindow), node)
                        });
                    }
    
                    var handles = NGM.Common.GetHandles(_findControl);
                    MainHandle = handles.Item1;
                    ControlHandle = handles.Item2;
                    NGM.API.Window.GetWindowRect(MainHandle, out Rectangle mainRectangle);
                    NGM.API.Window.GetWindowRect(ControlHandle, out Rectangle controlRectangle);
                    MainRectangle = mainRectangle;
                    ControlRectangle = controlRectangle;
    
                    if (MainView != null && MainView is NGM.Interface.IEditor)
                    {
                        (MainView as NGM.Interface.IEditor).RefreshProperty();
                    }
                }
            }

     

     

    마지막으로 추상 클래스(Abstract Class)인 BaseCustomToolModel이 상속 받고 있는 BaseModelICloneable 인터페이스를 재정의 합니다. 이 인터페이스는 기본 타입(Primitive Types)이 아닌 참조 타입(Reference Types)의 경우 Deep Copy(깊은 복사)를 처리해줍니다. 일반적으로 C++, Java, C# 과 같은 컴파일 언어들은 얕은 복사가 기본입니다. 얕은 복사의 경우 개체의 주소를 참조하므로 복사된 다른 액션의 속성 변경에 영향을 받게 됩니다. 얕은 복사(Shallow Copy)와 깊은 복사(Deep Copy)의 자세한 내용은 [ 여기 ]에서 확인할 수 있습니다.

            /// <summary>
            /// 이 액션을 깊은 복사하여 반환합니다.
            /// </summary>
            /// <returns>object는 복사한 액션입니다.</returns>
            public override object Clone()
            {
                var other = (InactiveClickModel)base.Clone();
                other.Children = new List<HandleItem>();
    
                foreach (var item in this.Children)
                {
                    other.Children.Add(new HandleItem() { Node = item.Node });
                }
    
                return other;
            }

     

     

    아래는 지금까지 작업한 전체 코드입니다.

    using NGM.Models.Item;
    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Drawing;
    using System.Drawing.Design;
    using System.Linq;
    using System.Xml.Serialization;
    
    namespace CustomInactiveClick
    {
        [Serializable]
        public class InactiveClickModel : NGM.Models.Interface.BaseCustomToolModel, NGM.Models.Interface.IHandleModel
        {
            private string _findControl;
    
            public override string DisplayCategory => "NGMsoftware";
    
            public override string DisplayName => "비활성 마우스 클릭";
    
            private Rectangle MainRectangle { get; set; }
    
            private Rectangle ControlRectangle { get; set; }
    
            [XmlIgnore]
            [Category("Data")]
            [DisplayName("매인 핸들")]
            [Description("컨트롤의 매인 핸들 값입니다.")]
            [Browsable(true)]
            [ReadOnly(true)]
            public IntPtr MainHandle { get; set; }
    
            [XmlIgnore]
            [Category("Data")]
            [DisplayName("컨트롤 핸들")]
            [Description("선택된 컨트롤의 핸들 값입니다.")]
            [Browsable(true)]
            [ReadOnly(true)]
            public IntPtr ControlHandle { get; set; }
    
            [Category("Action")]
            [DisplayName("컨트롤 제목")]
            [Description("선택한 컨트롤의 상단에 표시되는 제목입니다.")]
            [Browsable(true)]
            [DefaultValue(null)]
            public string Title { get; set; }
    
            [Category("Action")]
            [DisplayName("컨트롤 구조")]
            [Description("선택된 컨트롤의 구조입니다. 컨트롤을 찾기 위해 직접 편집이 가능합니다.")]
            [Browsable(true)]
            public List<NGM.Models.Item.HandleItem> Children { get; set; } = new List<NGM.Models.Item.HandleItem>();
    
            [Category("Action")]
            [DisplayName("마우스 좌표")]
            [Description("마우스 좌표를 설정합니다.")]
            [Browsable(true)]
            [DefaultValue(typeof(Point), "0, 0")]
            [Editor(typeof(NGM.Models.TypeEditor.MouseTrackingEditor), typeof(UITypeEditor))]
            public Point MousePoint { get; set; }
    
            public override void Execute()
            {
                var point = NGM.Common.ConvertToRelativePoint(this.ControlHandle, this.ControlRectangle, MousePoint);
                NGM.API.Window.PostMessage(ControlHandle, NGM.API.Options.WindowMessage.WM_LBUTTONDOWN, (int)NGM.API.Options.MouseAction.LeftDown, NGM.API.Window.MakeLParam(point));
                NGM.API.Window.PostMessage(ControlHandle, NGM.API.Options.WindowMessage.WM_LBUTTONUP, (int)NGM.API.Options.MouseAction.LeftUp, NGM.API.Window.MakeLParam(point));
            }
    
            [Category("Action")]
            [DisplayName("컨트롤 찾기")]
            [Description("응용 프로그램 또는 응용 프로그램의 컨트롤을 선택합니다.")]
            [Browsable(true)]
            [DefaultValue(null)]
            [Editor(typeof(NGM.Models.TypeEditor.FindControlEditor), typeof(System.Drawing.Design.UITypeEditor))]
            public virtual string FindControl
            {
                get { return _findControl; }
                set
                {
                    _findControl = value;
                    if (string.IsNullOrEmpty(_findControl))
                    {
                        this.Title = null;
                        this.MainHandle = IntPtr.Zero;
                        this.ControlHandle = IntPtr.Zero;
                        Children.Clear();
    
                        if (MainView != null && MainView is NGM.Interface.IEditor)
                        {
                            (MainView as NGM.Interface.IEditor).RefreshProperty();
                        }
    
                        return;
                    }
    
                    List<string> nodes;
                    nodes = _findControl.Split(new string[] { NGM.Definition.DOMSeparator }, StringSplitOptions.RemoveEmptyEntries).ToList();
                    Title = nodes.FirstOrDefault();
                    nodes.RemoveAt(0);
                    Children.Clear();
    
                    foreach (string node in nodes)
                    {
                        Children.Add(new HandleItem()
                        {
                            Node = (NGM.API.Options.FindWindow)Enum.Parse(typeof(NGM.API.Options.FindWindow), node)
                        });
                    }
    
                    var handles = NGM.Common.GetHandles(_findControl);
                    MainHandle = handles.Item1;
                    ControlHandle = handles.Item2;
                    NGM.API.Window.GetWindowRect(MainHandle, out Rectangle mainRectangle);
                    NGM.API.Window.GetWindowRect(ControlHandle, out Rectangle controlRectangle);
                    MainRectangle = mainRectangle;
                    ControlRectangle = controlRectangle;
    
                    if (MainView != null && MainView is NGM.Interface.IEditor)
                    {
                        (MainView as NGM.Interface.IEditor).RefreshProperty();
                    }
                }
            }
    
            /// <summary>
            /// 이 액션을 깊은 복사하여 반환합니다.
            /// </summary>
            /// <returns>object는 복사한 액션입니다.</returns>
            public override object Clone()
            {
                var other = (InactiveClickModel)base.Clone();
                other.Children = new List<HandleItem>();
    
                foreach (var item in this.Children)
                {
                    other.Children.Add(new HandleItem() { Node = item.Node });
                }
    
                return other;
            }
        }
    }

     

    프로젝트를 컴파일하세요. 아래 그림과 같이 프로젝트에서 우클릭 후 "다시 빌드"를 클릭하시면 됩니다.

    srgZm7a.png

     

     

    컴파일이 정상적으로 완료 되었으면, CustomInactiveClick 프로젝트에서 우클릭 후 "파일 탐색기에서 폴더 열기"를 선택하세요.

    l6uhRx7.png

     

     

    bin/Debug 폴더로 이동하세요. 만약, Release 모드로 컴파일했다면 bin/Release 폴더로 이동해야 합니다. 가급적이면 배포시에는 Release로 컴파일하세요. 아래 그림과 같이 CustomInactiveClick.dll이 있을겁니다. 이 라이브러리를 복사한 후 바탕화면에 붙여넣기하세요.

    G7YV33Z.png

     

     

    엔지엠 에디터를 실행한 후 "관리자" 메뉴의 "도구 모듈 설치" 버튼을 클릭하세요. (아래 그림은 함수 모듈 설치지만, 도구 모듈 설치로 해야 합니다.)

    ySJp52M.png

     

     

    모듈이 정상적이라면 아래와 같이 설치 작업이 성공했다는 메시지를 표시합니다.

    baqVyFP.png

     

     

    사용자 도구 상자에 보면 추가한 모듈의 액션이 추가된 것을 확인할 수 있습니다. 새로운 스크립트를 생성하고, 액션을 추가하세요.

    y9azjL4.png

     

     

    그림판의 핸들을 추가하고, 마우스 클릭 위치를 설정해줍니다. 그리고, 실행해보면 비활성으로 마우스가 클릭되는것을 알 수 있습니다.

    QqG8ssc.gif

     

     

    개발자에게 후원하기

    MGtdv7r.png

     

    추천, 구독, 홍보 꼭~ 부탁드립니다.

    여러분의 후원이 빠른 귀농을 가능하게 해줍니다~ 답답한 도시를 벗어나 귀농하고 싶은 개발자~

    감사합니다~

    • 네이버 공유하기
    • 페이스북 공유하기
    • 트위터 공유하기
    • 카카오스토리 공유하기
    추천0 비추천0

    댓글목록

    등록된 댓글이 없습니다.