NGMsoftware

NGMsoftware
로그인 회원가입
  • 매뉴얼
  • 학습
  • 매뉴얼

    학습


    C# C# .NET Core 매크로 프로그램 만들기. (비활성 매크로 만들기 1부)

    페이지 정보

    본문

    안녕하세요. 엔지엠소프트웨어입니다. 비활성 매크로 동작을 어떻게하면 쉽게 만들까 고민하다가 시간이 많이 흘렀네요. 엔지엠 6에서도 꾸준하게 요청이 있었던 내용들을 엔지엠 7(가칭)에서 개선하려다보니 수차례 시행착오가 있었습니다. 일반적으로 프로그램들은 각자의 창 제목을 가지고 있습니다. 하지만, 일부 프로그램은 동일한 이름으로 여러개가 실행되는데요. 엔지엠 매크로는 비활성 창을 유지하기 위해 창 제목을 내부에 저장해서 사용합니다.

     

    창 제목이 NGM 1, NGM 2, NGM 3과 같이 있다고 생각 해보세요. 매크로 프로그램이 프로그램을 인식할 때 실행중인 프로그램의 창 제목을 저장하고 있다가 해당 창이 있으면 핸들을 다시 잡아서 사용합니다. 창 제목은 변하지 않지만, 신호를 줄 창의 핸들은 랜덤하게 변화하기 때문에 핸들 값을 저장해봐야 의미가 없습니다. 변하지 않는 값을 활용해야 다음에 매크로를 실행하더라도 동일한 동작을 수행할 수 있습니다. 윈도우에서 프로그램이 실행될 때 핸들 값을 유니크하게 생성하는데요. 이 때 핸들은 랜덤하게 부여됩니다.

     

    그래서, 엔지엠 매크로의 창 제목 변경 액션을 이용해서 하나씩 변경해가면서 핸들을 하나씩 잡아주면 멀티 다클라도 비활성으로 동작시킬 수 있습니다. 하지만, 창 제목을 변경할 수 없는 경우나 창 제목을 강제로 변경하면 매크로가 감지되는 경우등등... 다양한 환경에 대응할 수 없는 경우들이 있습니다. 그래서, 엔지엠 7에서는 창 제목을 변경하지 않더라도 비활성 멀티 다클라가 동작하도록 디자인되었습니다. 그리고, 마우스, 키보드와 같은 디바이스 신호를 어떻게 처리할지도 글로벌하게 처리할 수 있도록 했습니다. 이 부분은 다음에 알아보기로 하고, 오늘은 비활성 매크로에 대해 알아볼께요.

     

    엔지엠 매크로에서 데이타를 통신하기 위해 인터페이스를 만들어줍니다. IHandle 인터페이스는 아래와 같습니다. 마우스와 키보드 또는 기타 디바이스의 신호를 입력하기 위해서는 ControlHandle만 필요합니다. 하지만, 일부 기능들은 MainHandle도 필요하기 때문에 IHandle 인터페이스는 2개의 정보를 모두 가지고 있어야 합니다. 참고로, MainHandle이 수행하는 기능들은 창 이름 변경, 창 위치 변경, 창 크기 변경등등... 여러가지가 있습니다.

    namespace Ai.Interface
    {
        public interface IHandle
        {
            IntPtr MainHandle { get; set; }
    
            IntPtr ControlHandle { get; set; }
        

     

    IMouseTracker 인터페이스도 추가 해줍니다. 이 인터페이스를 상속 받으면 Coordinate(좌표)를 추적해서 선택할 수 있는 Attribute를 사용할 수 있습니다. 마우스 클릭, 마우스 다운과 업, 마우스 이동등등... 마우스 좌표를 입력해야 하는 모든 액션에 추가해야 합니다. 이외에도 마우스 좌표를 이용해서 핸들을 찾거나 웹 엘리먼트를 찾을 때도 필요하기 때문에 마우스 동작외에도 인터페이스로 사용할 수 있도록 해줘야 합니다.

    namespace Ai.Interface
    {
        public interface IMouseTracker
        {
            System.Drawing.Point Coordinate { get; set; }
        }
    }

     

    프로그램을 제어하기 위해 핸들이 필요한 액션들이 많습니다. 그래서, 핸들이 필요한 액션들은 모두 아래와 같이 BaseModel을 상속 받아야 합니다. 이렇게 핸들 관련된 기능들을 추상화하면 불필요한 코딩을 줄일 수 있습니다. 추상 클래스는 new 로 인스턴스화 할 수 없으므로, 실제 구현 클래스는 따로 만들어줘야 합니다.

    public abstract class BaseModel : ActionModel, IHandle, IMouseTracker

     

    AddHandleModel은 핸들을 추가하기 위한 구현 클래스입니다. 위의 추상 클래스를 상속 받고 있습니다.

    public class AddHandleModel : BaseModel

     

    AddHandleModel의 핵심 내용은 아래와 같습니다. 옵션을 이용해서 싱글 또는 멀티로 핸들을 추가하도록 했습니다.

            [LocalizedCategory("Action")]
            [LocalizedDisplayName("AddOption")]
            [LocalizedDescription("AddOption")]
            [Browsable(true)]
            [DefaultValue(typeof(Ai.Definition.HandleAddOption), "AddSelectOne")]
            public Ai.Definition.HandleAddOption AddOption { get; set; } = Definition.HandleAddOption.AddSelectOne;

     

    핸들을 추가하기 위한 옵션인데요. 사용자가 선택할 수 있는 항목은 아래와 같습니다.

    • AddSelectOne: 선택한 핸들 하나만 기존 핸들 목록에 추가
    • AddSelectAll: 선택한 핸들 모두 기존 핸들 목록에 추가
    • OverwriteSelectOne: 등록된 핸들을 모두 삭제하고, 선택한 핸들 하나만 추가
    • OverwriteSelectAll: 등록된 핸들을 모두 삭제하고, 선택한 핸들 모두 추가
            public enum HandleAddOption
            {
                AddSelectOne,
                AddSelectAll,
                OverwriteSelectOne,
                OverwriteSelectAll
            }

     

    동일한 이름을 가진 핸들일수도 있고, 각자 이름이 다른 핸들일수도 있습니다. 그리고, A 그룹의 핸들 여러개를 사용하다가 B 그룹의 핸들 여러개를 따로 동작시킬수도 있겠죠? 어쩌면 A 그룹과 B 그룹을 모두 실행할수도 있을겁니다. 그렇기 때문에 기존 핸들 목록에 추가하거나 덮어쓰기가 필요합니다. 다양한 시나리오에 모두 부합하려다보니 옵션이 많아지는 문제가 있지만 4개정도는 로직을 구성하는데 큰 문제가 안될거 같긴합니다.

     

    Execute 메소드에서 핸들을 처리합니다. 사용자가 선택한 옵션에 따라 플레이어에 핸들 목록을 채워줍니다.

            public override void Excute(IPlayer player)
            {
                base.Excute(player);
    
                if (player.MainHandles == null)
                    player.MainHandles = new List<IntPtr>();
    
                if (player.ControlHandles == null)
                    player.ControlHandles = new List<IntPtr>();
    
                switch (AddOption)
                {
                    case Definition.HandleAddOption.AddSelectOne:
                        player.MainHandles.Add(MainHandle);
                        player.ControlHandles.Add(ControlHandle);
                        break;
                    case Definition.HandleAddOption.AddSelectAll:
                        if (MainHandles != null && ControlHandles != null)
                        {
                            player.MainHandles.AddRange(MainHandles);
                            player.ControlHandles.AddRange(ControlHandles);
                        }
                        break;
                    case Definition.HandleAddOption.OverwriteSelectOne:
                        player.MainHandles = new List<IntPtr> { MainHandle };
                        player.ControlHandles = new List<IntPtr> { ControlHandle };
                        break;
                    case Definition.HandleAddOption.OverwriteSelectAll:
                        player.MainHandles = MainHandles;
                        player.ControlHandles = ControlHandles;
                        break;
                }
            }

     

    비활성 멀티 핸들 상황에서 마우스 클릭을 테스트 해볼건데요. 마우스 클릭도 비활성으로 입력할 수 있도록 변경해야 합니다. 앞서 설명한 내용을 아래와 같이 수정하세요.

            public override void Excute(Ai.Interface.IPlayer player)
            {
                base.Excute(player);
    
                Ai.Definition.DeviceInputType inputType = Definition.DeviceInputType.Windows;
                if (this.InputType == Definition.ChildDeviceInputType.Inheritance)
                    inputType = player.DeviceInputType;
                else
                    inputType = (Ai.Definition.DeviceInputType)Enum.Parse(typeof(Ai.Definition.DeviceInputType), this.InputType.ToString());
    
                switch (inputType)
                {
                    case Definition.DeviceInputType.Windows:
                    case Definition.DeviceInputType.Event:
                    case Definition.DeviceInputType.Input:
                        Ai.Common.Mouse.Click(this.Coordinate, this.MouseButton, inputType, this.UseRelative);
                        break;
                    case Definition.DeviceInputType.PostMessage:
                    case Definition.DeviceInputType.SendMessage:
                        if (player.ControlHandles != null)
                        {
                            foreach (var handle in player.ControlHandles)
                            {
                                Point coordinate = InactiveMouseLocation(handle, this._windowRect, this.Coordinate);
                                Ai.Common.Mouse.Click(coordinate, this.MouseButton, inputType, this.UseRelative, controlHandle: handle);
                            }
                        }
                        break;
                }
            }

     

    여기서부터 약간 고민이 생깁니다. 일부 프로그램들은 기준이 되는 윈도우를 마우스 좌표로 찾지 못하는 경우가 있습니다. 그래서, 기준 윈도우 위치를 처리하는 _windowRect를 private이 아닌 public으로 오픈해야할지 결정하지 못했습니다. 현재는 private이라서 사용자가 신경쓰지 않아도 되는데요. 추후에는 _windowRect를 처리하지 못할 때 메세지를 표시할지 아니면 직접 수정할 수 있도록 속성을 public으로 할지 리팩토링할 때 결정해야겠습니다

     

    아래는 PostMessage를 처리하는 user32 API입니다.

            [return: MarshalAs(UnmanagedType.Bool)]
            [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
            internal static extern bool PostMessage(IntPtr hWnd, uint Msg, int wParam, int lParam);

     

    SendMessage는 아래와 같습니다. PostMessage는 비동기 방식이고, SendMessage는 동기 방식입니다. 비동기는 윈도우가 마우스가 신호를 처리하지 않더라도 다음 메세지를 받습니다. 하지만, SendMessage는 마우스가 신호를 처리할 때까지 기다립니다. SendMessage를 잘 사용하지 않는 이유는 마우스 신호를 처리하다가 문제가 발생했을 때 프로그램이 멈추기 때문입니다. 처리 여부 신호를 줄 때까지 대기하기 때문입니다.

            [DllImport("user32.dll")]
            internal static extern int SendMessage(IntPtr hWnd, int Msg, int wParam, int lParam);

     

    위와 같은 문제를 해결하기 위해 SendMessageTimeout API가 새롭게 추가되었습니다. 아무래도 새로 추가된 SendMessageTimeout을 사용하도록 개선하는게 좋을거 같네요. 이 함수를 사용하면 기존에 윈도우에 문제가 발생했을 때 프로그램이 먹통되는 문제를 해결할 수 있습니다.

            [DllImport("user32", SetLastError = true, CharSet = CharSet.Auto)]
            internal static extern IntPtr SendMessageTimeout(
                IntPtr windowHandle, 
                uint message, 
                int wordParameter, 
                int longParameter, 
                Options.SendMessageTimeoutFlag flag, 
                uint timeout, 
                out IntPtr resultHandle);

     

    이제 비활성 마우스 신호를 처리할 수 있는 API를 모두 만들었으니 간단하게 테스트를 해볼까요? 아래와 같이 동일한 이름을 가진 그림판을 여러개 실행했습니다.

    UPh37J2.png

     

     

    매크로 프로그램도 실행한 후 아래와 같이 액션들을 추가 해보세요. 아직... 정식으로 배포된게 아니라서 프로그램이 없습니다. 아마도, 내년쯤 사용해볼 수 있을거 같아요^^;

     

     

    디바이스 입력 방식은 다양합니다. 현재는 총 5가지인데요. 앞으로 시리얼 통신, 아두이노, 오토핫키, 파이썬, 클래스디디, 인터셉션등등... 추가될게 많습니다. 엔지엠 6에서는 디바이스 신호 처리가 여러곳에 분산되어 있어서 일관성을 해치고 있다고 생각합니다. 그래서, 엔지엠 7에서는 최상위에 어떤 입력 장치를 사용할지 먼저 결정하면 아래 모든 액션들이 이 내용을 토대로 동작하게끔 할 예정입니다. 비활성 동작을 테스트하는 거리서 입력 방식을 PostMessage로 선택했습니다.

    6FShJLJ.png

     

     

    핸들 추가 액션에서 그림판을 선택 해보세요. 핸들 목록에 같은 이름을 가진 그림판 모두 핸들을 가져온걸 확인할 수 있습니다.

     

     

    핸들을 추가하면 기본적으로 하나의 창만 등록됩니다. 그래서, 모든 창을 한번에 제어하려면 아래와 같이 추가 옵션을 변경해줘야 합니다.

    qP9oI2B.png

     

     

    추가 옵션에서 AddSelectOne을 선택했다면 프로그램 인덱스에 따라서 하나만 추가됩니다. 인덱스는 프로그램이 실행된 순서입니다. 화면에 놓여있는 순서는 아닙니다.

    tq8h9ab.png

     

     

    일단 모두 추가하기로 했으니 플레이어에 핸들이 3개 등록되었을겁니다. 이제 마우스 클릭 좌표를 설정 해볼께요. 마우스 좌표 설정은 첫번째 실행된 프로그램에서 작업해줘야 합니다. 이유는 엔지엠 6의 다클라 매크로 만들기와 동일합니다. 창은 3개인데 모두 위치가 다릅니다. 하지만, 마우스 좌표 설정은 한번만 할건데요. 이렇게되면 첫번째 창은 위치를 계산할 수 있지만, 두번째 창의 위치를 알아도 마우스 좌표를 어떻게 계산해야 할지 기준이 없어서 처리가 불가능합니다. 그리고, 창이 이동되었다면 이전 위치와 이동된 위치만큼 마우스 좌표에 보정해줘야 처음 의도한 위치에 클릭이 됩니다.

     

    아래 동영상을 참고해서 마우스 좌표를 설정해보세요.

     

     

    위에서 잠깐 언급했었는데요. 마우스, 키보드 및 기타 장치 신호를 윈도우에 보내기 위해서 최상위에 글로벌 설정을 한다고 했습니다. 하지만, 싱글 모드에서는 글로벌 설정이 필요 없을수도 있습니다. 모두가 멀티 다클라를 사용하는건 아니니까요. 그래서, 디바이스 입력 액션들은 모두 입력 방식을 속성으로 가집니다. 기본값은 Inheritance인데요. 설정을 상위에서 상속 받는다는 의미입니다. 장치 입력 방법 액션으로 글로벌 설정이 된 경우 이 설정을 상속 받습니다. 개별 설정하려면 다른 옵션을 선택하면 됩니다.

    SyA6Xpe.png

     

     

    완성된 매크로를 실행 해볼께요. 아래 동영상과 같이 동일한 이름을 가진 그림판 모두에 마우스 클릭이 동일하게 작동합니다.

     

     

    이렇게 멀티 다클라 비활성 기능을 디자인했는데요. 기존 엔지엠 매크로와는 다르게 하나의 매크로를 완성하면 여러개의 동일한 프로그램에 다클라로 매크로를 실행할 수 있습니다. 지금은 아이디어 단계지만, 이미지 서치나 이미지 매치 그리고 각종 루틴을 이동시키는 로직에서 하나의 스크립트이지만 개별적으로 이미지를 인식해서 로직을 분기하도록 할겁니다. 이렇게하면 각각 매크로를 만든것과 같은 효과가 나올겁니다. 지금은 스크립트를 여러개 복사해서 핸들을 각각 변경해야 하는 번거로움이 있었는데요. 차세대 매크로에서는 좀 더 사용이 편리할거 같습니다^^

     

    개발자에게 후원하기

    MGtdv7r.png

     

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

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

    감사합니다~

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

    댓글목록

    등록된 댓글이 없습니다.