NGMsoftware

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

    학습


    C# C# .NET 매크로 프로그램 만들기. (기계식 또는 하드웨어 방식 마우스 매크로 with Interception)

    페이지 정보

    본문

    안녕하세요. 엔지엠소프트웨어입니다. 요즘 하드웨어 또는 기계식 매크로 제작 방법에 대해서 계속 개발하고 글을 올리고 있는데요. 오늘이 마지막이 될듯 합니다. 개발을 하다보니 다양한 경우의 수에 대응하려고 만들었던 몇가지 기능들과 개념적으로 충돌이 발생해서 한동안 멍때리고 있었어요. 순간적으로 이게 맞나 싶다가도 아닌거 같아서 다시 고쳤다가 다시 되돌리기를 수차례 반복하다가 결국은 안하는걸로 결론 내렸어요.

     

    범용성과 편의성을 모두 잡으려고, 매크로 상단에 글로벌 셋팅을 만들어 두었는데요. 이 설정에 인헤리턴스(Inheritance)가 있습니다. 디바이스 장치의 정보를 글로벌하게 설정하면 이 후 모든 마우스와 키보드 액션들이 이 설정 정보를 따라가는거거든요. 그런데, 항상 그렇듯이 어떤 특수한 상황에서는 마우스나 키보드를 다른 설정으로 변경하고 싶을겁니다. 이런 경우 인헤리턴스가 아닌 자체 설정으로 변경해야 하거든요. 여기서 충돌이 발생하는건 하드웨어 설정과 소프트웨어 설정이 중복되는 경우 상속이 아니라면 어떤 설정을 따라가야 할까를 고민중에 있었습니다.

     

    일단은, 하드웨어는 하드웨어고 소프트웨어는 소프트웨어 설정을 따라가는게 맞을듯한데요. 이 부분은 나중에 실제로 유저분들이 사용해보고 피드백을 주시면 고쳐야 할거 같아요. 현재로써는 이 방식이 가장 합리적인듯 합니다. 

     

    대부분 코드는 클래스 디디와 동일합니다. 속성들도 똑같이 추가하면 될거 같아요.

    [LocalizedCategory("Action")]
    [LocalizedDisplayName("SelectModule")]
    [LocalizedDescription("SelectModule")]
    [Browsable(true)]
    [DefaultValue(null)]
    [Editor(typeof(TypeEditor.OpenFileSelectorEditor), typeof(UITypeEditor))]
    public string? SelectModule { get; set; }

     

    클래스디디는 모듈이 하나뿐이라서 디바이스를 선택할 필요가 없는데요. 인터셉션은 설치된 드라이버가 모두 표시됩니다. 이중에 하나를 선택해서 Interception 라이브러리를 메모리에 올려야 합니다. 그래서, 아래와 같이 사용자가 마우스와 키보드의 디바이스를 선택할 수 있도록 속성을 추가해야 합니다.

    [LocalizedCategory("Option")]
    [LocalizedDisplayName("MouseDeviceID")]
    [LocalizedDescription("MouseDeviceID")]
    [Browsable(true)]
    [DefaultValue(null)]
    [TypeConverter(typeof(TypeConverter.InputDeviceConverter))]
    public string MouseDeviceID { get; set; }
    
    [LocalizedCategory("Option")]
    [LocalizedDisplayName("KeyboardDeviceID")]
    [LocalizedDescription("KeyboardDeviceID")]
    [Browsable(true)]
    [DefaultValue(null)]
    [TypeConverter(typeof(TypeConverter.InputDeviceConverter))]
    public string KeyboardDeviceID { get; set; }

     

    InputDeviceConverter 클래스를 하나 만듭니다. 컨버터는 PropertyGrid에 매핑된 모델의 속성을 콤보박스나 달력 또는 사용자가 만든 커스텀 폼(Form)을 표시할 때 사용합니다. 이 인터페이스는 텍스트나 오브젝트를 특정 폼에 표시하고, 데이타를 가공해서 반환하게끔 만들어줍니다. 정말 단순한 작업이라면 딱히 컨버터를 개발할 필요는 없지만, 엔지엠 매크로와 같이 화면 캡쳐, 사각형 영역 지정, 마우스 트레킹등등... 다양한 환경에서 사용자 편의를 위해 폼을 제공해야 한다면 어쩔 수 없이 모두 다 만들어야 합니다.

     

    컨버터 제작에 대한 내용은 초반에 한번 다뤘기 때문에 여기에서는 간단하게 어떤 방식인지 알아보고, 오래전 부터 알고 있었던 버그를 약간의 꼼수로 해결 해보도록 할께요.

    internal class InputDeviceConverter : System.ComponentModel.TypeConverter

     

    컨버터는 어떤 이유인지는 모르겠지만, 특정 속성에서 두번 이벤트가 발생합니다. 사실, 사용하는데 큰 의미는 없습니다. 거의 찰나의 순간에 처리되기 때문입니다. 하지만, 속성이 데이타베이스에서 정보를 가져와서 표시해야 한다거나 네트워크로 요청한 데이터를 가공해서 보여줘야 한다면 문제가 커집니다. 한번 요청으로 가져온 데이터를 표시하기만 하면 되는데요. 이걸 한번 더 요청하면 1초 걸릴일이 2초가 걸리기 때문입니다. 만약, 시간이 정말 오래 걸리는 작업이라면 정말 큰 문제가 되겠죠?

     

    기본 값을 제공하도록 설정해줍니다.

    public override bool GetStandardValuesSupported(ITypeDescriptorContext? context)
    {
        return true;
    }

     

    폼을 표시할 수 있는지를 반환하고, 아래와 같이 폼에서 선택한 값을 반환해주면 프로퍼티 그리드의 속성에 데이터를 추가할 수 있습니다.

    public override object? ConvertFrom(ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object value)
    {
        if (value is string)
        {
            if (_items == null)
                SetData(context);
    
            if (_items == null)
                return null;
    
            var item = (_items.Cast<Data.KeyValueItem>()).Where(w => w.ToString() == value.ToString()).FirstOrDefault();
            if (item?.Value == null)
                return value;
            else
                return item.Key;
        }
        return null;
    }

     

    중요한 부분은 아래 코드입니다. 이 코드가 이벤트를 2번 발생시킵니다. 그래서, Tag가 NULL인지 판단하고, 값을 넣고 빼면서 스위칭 해줍니다. 이렇게하면 이 로직 아래에 있는 데이타를 가져오는 메소드를 한번만 실행시킬 수 있습니다. SetData가 두번씩 실행되는 문제를 해결할 수 있습니다. 관련 내용을 좀 검색해봤는데요. Constructor가 없으면 그렇다는거 같더라고요. 이와 관련된 이슈가 스택오버플로우에 아직도 열려 있는거 같았습니다.

    public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context)
    {
        if (((System.Windows.Forms.GridItem)context).Tag == null)
            ((System.Windows.Forms.GridItem)context).Tag = new object();
        else
        {
            ((System.Windows.Forms.GridItem)context).Tag = null;
            return new StandardValuesCollection(_items);
        }
    
        _filterKey = context?.PropertyDescriptor?.GetValue(context.Instance)?.ToString();
        SetData(context);
        return new StandardValuesCollection(_items);
    }

     

    SetData 메소드에서는 인터셉션의 마우스와 키보드 디바이스 목록을 가져옵니다.

    if (context.PropertyDescriptor.Name == "MouseDeviceID")
        _items = new Data.Factory().GetMouseDeviceNames(model);
    else if (context.PropertyDescriptor.Name == "KeyboardDeviceID")
        _items = new Data.Factory().GetKeyboardDeviceNames(model);

     

    데이타를 가져오는 부분의 코드입니다. 이 코드는 Data 그룹의 팩토리(Factory)가 담당합니다.

    internal List<BaseItem> GetMouseDeviceNames(Ai.Model.ExternalAPI.Interception.ConnectionModel model)
    {
        List<BaseItem> result = new List<BaseItem>();
        var context = new Ai.Api.InterceptionManager.Manager(0, 0);
        var deviceList = context.GetDeviceList();
        foreach (var device in deviceList)
        {
            if (device.IsMouse)
                result.Add(new KeyValueItem() { Key = $"{device.Handle}:{device.Id}", Value = $"{device.Handle}:{device.Id}" });
        }
        context.Dispose();
        return result;
    }

     

    이제 마우스 부분을 처리해야 합니다. 그리고, 키보드도 개발해야 하는군요. 생각해보니 클래스 디디도 아직 키보드는 개발하지 않았더라고요. 이 내용이 끝나면 다음에는 클래스 디디와 인터셉션의 키보드 인터페이스도 개발해야 할거 같네요. 아무튼, 마우스는 클릭, 더블클릭, 다운과 업, 휠 그리고 이동 및 드래그가 있습니다. 생각보다 내용이 많더라고요. 이걸 하나씩 다 구현해줘야 합니다. 그래도, 클래스 디디와 인터셉션은 홈페이지에 도움말이 너무 잘 되어 있어서 참고해서 만드는데 어려움은 없었습니다. 테스트가 귀찮을뿐이죠^^;

     

    인풋 디바이스(Input Device) 형식에 따라서 코드는 분기되어 있습니다. 소프트웨어 신호, 하드웨어 신호가 분리되어 있어서 이 부분에 코딩하면 됩니다. 이전 시간에 만든 클래스 디디 아래쪽에 코드를 추가하면 될거 같네요.

    case Definition.DeviceInputType.Interception:
        if (UseRandom)
            coordinate.Offset(Ai.Common.Helper.RandomPosition(RandomPointMin, RandomPointMax, Options, player.UseSpecialRandomLocation, player.SpecialRandomLocationMean));
    
        useCurrentCoordinate = false;
    
        if (player.Manager.InterceptionAutoCorrection)
        {
            Ai.Common.Mouse.Move(player, coordinate, player.Manager.InterceptionMouseDistance, player.Manager.InterceptionMouseSpeed, inputType, player.Manager.InterceptionUseRelative);
            useCurrentCoordinate = true;
        }
    
        Ai.Common.Mouse.Run(player, Ai.Definition.MouseAction.Click, coordinate, this.MouseButton, inputType, player.Manager.InterceptionUseRelative, useCurrentCoordinate, mouseDownUpDelay: player.Manager.InterceptionMouseDownUpDelay);
        break;

     

    마우스 클릭 액션의 Run에서 각각의 설정에 따라서 동작하도록 로직이 분기되어 있어요.

    case Api.MouseKeyboardManager.MouseSimulator.MouseButton.Left:
        player.Manager.Interception.SendMouseButtonEvent(player.Manager.Interception.MouseDeviceID, 0, 1);
        Task.Delay(mouseDownUpDelay.Value).Wait();
        player.Manager.Interception.SendMouseButtonEvent(player.Manager.Interception.MouseDeviceID, 0, 0);
        break;
    case Api.MouseKeyboardManager.MouseSimulator.MouseButton.Right:
        player.Manager.Interception.SendMouseButtonEvent(player.Manager.Interception.MouseDeviceID, 1, 1);
        Task.Delay(mouseDownUpDelay.Value).Wait();
        player.Manager.Interception.SendMouseButtonEvent(player.Manager.Interception.MouseDeviceID, 1, 0);
        break;
    case Api.MouseKeyboardManager.MouseSimulator.MouseButton.Middle:
        player.Manager.Interception.SendMouseButtonEvent(player.Manager.Interception.MouseDeviceID, 2, 1);
        Task.Delay(mouseDownUpDelay.Value).Wait();
        player.Manager.Interception.SendMouseButtonEvent(player.Manager.Interception.MouseDeviceID, 2, 0);
        break;

     

    이제 만들어진 내용으로 테스트를 해볼까요? 마우스 클릭부터 드래그까지 하나씩 해볼께요.

     

     

    개발자에게 후원하기

    MGtdv7r.png

     

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

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

    감사합니다~

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

    댓글목록

    등록된 댓글이 없습니다.