NGMsoftware

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

    학습


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

    페이지 정보

    본문

    안녕하세요. 엔지엠소프트웨어입니다. 드디어~ 기계식 마우스 매크로 또는 하드웨어 방식의 마우스와 키보드 매크로를 만드는 방법까지 오게 되었습니다. 지금까지는 윈도우 API를 이용해서 소프트웨어 방식으로 신호를 전달하는 기능들을 중점적으로 개발했는데요. 소프트웨어 방식은 크게 2가지로 나누어집니다. 첫번째는 비활성 모드고, 두번째는 활성 모드입니다.

     

    비활성 모드는 마우스와 키보드 신호를 선택한 프로그램에 직접 명령을 전달하기 때문에 사용자는 다른 작업을 진행할 수 있습니다. 효율적으로 컴퓨터를 사용할 수 있게 됩니다. 그리고, 여러개의 다클라 환경에서도 마우스와 키보드 입력에 대해 간섭 없이 안정적으로 자동화가 가능합니다. 이 부분이 가장 큰 장점인데요. 문제는 비활성 신호가 100프로 동작하는건 아니라는점입니다. 그래서, 활성 모드로 이용해야 하는 경우들도 있습니다.

     

    활성 모드는 프로그램에 마우스와 키보드 신호를 주는게 아닌 윈도우에 신호를 주는 방식입니다. 그래서 윈도우에 연결되어 있는 마우스와 키보드를 사용할 수 없습니다. 동시에 두명이 마우스와 키보드를 조작한다고 생각하시면 됩니다. 우리가, 메모장에 글자를 타이핑할 때 메모장에 텍스트를 입력할 부분을 클릭해놓고, 키보드를 타이핑하는데요. 만약, 매크로가 다른 작업을 하기 위해서 다른곳을 클릭했다면 타이핑 작업이 의도하지 않은 곳에서 이루어지게됩니다. 비활성보다 효율은 떨어질수밖에 없습니다.

     

    활성 모드라고 해서 멀티 다클라가 불가능한건 아닙니다. 비활성 모드보다 효율은 떨어지지만, 여러개의 다클라에서 매크로 작업이 가능하긴 합니다. 자세한 내용은 이전 글들을 참고하시면 도움이 될거 같네요. 오늘은 활성 모드중에 하나인 기계식 또는 하드웨어 방식을 알아볼건데요. 테스트를 위해서는 아두이노라는 장치가 별도로 필요합니다. 아무래도 하드웨어 방식으로 자연스럽게 신호를 전환하려면 중간에 장치가 필요하기 때문입니다. 만약, 아두이노와 같은 시리얼 통신 장치가 없더라도 하드웨어 방식을 사용하지 못하는건 아닙니다. 엔지엠 6에 기본으로 제공하는 클래스DD나 인터셉션과 같은 소프트웨어 드라이버를 사용해서 하드웨어 또는 기계식 신호로 변환할 수 있습니다.

     

    우선은 시리얼 통신에 필요한 콤포트(COM Port)를 사용자가 선택할 수 있도록 폼을 하나 만들어야 합니다.

    xFBnzNI.jpeg

     

     

    디자인은 심플하죠? 콤보박스에 시리얼 포트 목록을 표시하고, 사용자가 선택하면 저장하는 단순한 기능을 포함하고 있습니다. 팝업으로 표시할거라서 부모폼 또는 매인폼에서 창 정보를 가져갈 수 있도록 속성을 하나 추가했습니다.

    public string? ComPort { get; private set; }

     

    생성자는 클라이언트를 인자로 넘겨 받습니다.

    public ComPortSelectEditorPopup(Ai.Interface.IClient client, string[] ports)
    {
        _client = client;
        InitializeComponent();
        
        cboSerialPort.Items.AddRange(ports);
    }

     

    참고로, 부모폼에서 포트 목록을 넘겨줘도 됩니다. 아니면, 생성자의 이니셜라이저 아래에 시리얼 포트 목록을 가져와도 됩니다. 아래 코드는 시리얼 포트 목록을 가져오는 메소드인데요. 마이크로소프트에서 기본으로 제공해주는 라이브러리라서 쉽게 사용이 가능합니다.

    var ports = System.IO.Ports.SerialPort.GetPortNames();

     

    다시 팝업 폼으로 돌아와서 OK 버튼을 클릭했을 때 처리 로직을 만들어줍니다.

    private void btnOK_Click(object sender, EventArgs e)
    {
        if (string.IsNullOrEmpty(cboSerialPort.Text))
        {
            if (MessageBox.Show(this, _client.ResxMessage.GetString("NotSelectComPortAndClose"), "Warning", MessageBoxButtons.YesNo, MessageBoxIcon.Warning) == DialogResult.Yes)
            {
                DialogResult = DialogResult.Cancel;
                this.Close();
            }
        }
        else
        {
            ComPort = cboSerialPort.Text;
            DialogResult = DialogResult.OK;
            this.Close();
        }
    }

     

    취소했을 때는 정보를 초기화하고, 창을 닫아줍니다.

    private void btnCancel_Click(object sender, EventArgs e)
    {
        DialogResult = DialogResult.Cancel;
        ComPort = null;
    }

     

    여기까지만 코딩을해도 동작하는데 전혀 문제가 없습니다. 하지만, Dialog를 사용한다면 폼에서 Accept 버튼과 Cancel 버튼을 설정하는게 좋습니다.

    t6xGZtB.jpeg

     

     

    시리얼 통신을 초기화할 Connection 모델을 하나 추가했습니다. 아직 구현은 안했지만, 미리 Disconnection 모델도 추가만 해두었습니다.

    2HXxZ2v.jpeg

     

     

    아래는 SerialPort 개체의 속성을 동일하게 모델의 속성으로 만들었습니다. 아두이노는 8비트 모듈이라서 몇가지 제약이 있긴하지만, 이런 제약들을 뛰어 넘을 수 있는 우회 방법들도 있습니다. 조금만 아이디어를 내면 좀 더 쾌적하게 사용할 수 있긴합니다. 다만, 우회하기 위해서는 몇가지 테크닉도 필요해서 내용은 다소 복잡한 부분이 있습니다.

    [LocalizedCategory("Setting")]
    [LocalizedDisplayName("SerialDtrEnable")]
    [LocalizedDescription("SerialDtrEnable")]
    [Browsable(true)]
    [DefaultValue(false)]
    public bool DtrEnable { get; set; }
    
    [LocalizedCategory("Setting")]
    [LocalizedDisplayName("SerialRtsEnable")]
    [LocalizedDescription("SerialRtsEnable")]
    [Browsable(true)]
    [DefaultValue(false)]
    public bool RtsEnable { get; set; }
    
    [LocalizedCategory("Setting")]
    [LocalizedDisplayName("SerialBaudRate")]
    [LocalizedDescription("SerialBaudRate")]
    [Browsable(true)]
    [DefaultValue(9600)]
    public int BaudRate { get; set; } = 9600;
    
    [LocalizedCategory("Setting")]
    [LocalizedDisplayName("SerialDataBits")]
    [LocalizedDescription("SerialDataBits")]
    [Browsable(true)]
    [DefaultValue(8)]
    public int DataBits { get; set; } = 8;
    
    [LocalizedCategory("Setting")]
    [LocalizedDisplayName("SerialReadTimeout")]
    [LocalizedDescription("SerialReadTimeout")]
    [Browsable(true)]
    [DefaultValue(1000)]
    public int ReadTimeout { get; set; } = 1000;
    
    [LocalizedCategory("Setting")]
    [LocalizedDisplayName("SerialWriteTimeout")]
    [LocalizedDescription("SerialWriteTimeout")]
    [Browsable(true)]
    [DefaultValue(1000)]
    public int WriteTimeout { get; set; } = 1000;
    
    [LocalizedCategory("Setting")]
    [LocalizedDisplayName("SerialParity")]
    [LocalizedDescription("SerialParity")]
    [Browsable(true)]
    [DefaultValue(typeof(System.IO.Ports.Parity), "None")]
    public System.IO.Ports.Parity Parity { get; set; } = System.IO.Ports.Parity.None;
    
    [LocalizedCategory("Setting")]
    [LocalizedDisplayName("SerialStopBits")]
    [LocalizedDescription("SerialStopBits")]
    [Browsable(true)]
    [DefaultValue(typeof(System.IO.Ports.StopBits), "One")]
    public System.IO.Ports.StopBits StopBits { get; set; } = System.IO.Ports.StopBits.One;

     

    아래는 시리얼포트와는 상관이 없지만, 운영상 필요한 속성들입니다.

    [LocalizedCategory("Action")]
    [LocalizedDisplayName("SerialPortName")]
    [LocalizedDescription("SerialPortName")]
    [Browsable(true)]
    [DefaultValue(null)]
    [TypeConverter(typeof(TypeConverter.SerialPortConverter))]
    public string? SerialPortName { get; set; }
    
    [LocalizedCategory("MouseSpeed")]
    [LocalizedDisplayName("Speed")]
    [LocalizedDescription("MouseSpeed")]
    [Browsable(true)]
    [DefaultValue(90)]
    public int Speed { get; set; } = 90;
    
    [LocalizedCategory("MouseSpeed")]
    [LocalizedDisplayName("Distance")]
    [LocalizedDescription("MouseDistance")]
    [Browsable(true)]
    [DefaultValue(30)]
    public int Distance { get; set; } = 30;
    
    [LocalizedCategory("Action")]
    [LocalizedDisplayName("UseMouseMove")]
    [LocalizedDescription("UseMouseMove")]
    [Browsable(true)]
    [DefaultValue(false)]
    public bool AutoCorrection { get; set; }

     

    시리얼포트에 연결이 되었다고해서 작업이 마무리 되는건 아닙니다. 앞서 말했듯이 아두이노는 8비트 모듈이라서 마우스와 키보드를 처리할 때 제약이 있습니다. 쉽게 마우스의 경우에는 256까지만 데이터를 처리할 수 있습니다. 그래서, 좌표 값을 설정할 때 256을 넘으면 1로 돌아가기 때문에 최대치를 넘게 설정하면 안됩니다. 그리고, 실제로 사용해보면 256까지도 사용할 수 없습니다.

     

    마우스는 좌표계를 나타내기 때문에 -127~127까지만 표현할 수 있습니다. 상하좌우는 기준 좌표에서 마이너스 값이 될수도 있고 플러스 값이 될수도 있기 때문입니다. 이런 제약 때문에 마우스가 이동할 때 계산이 복잡해지는 부분이 있습니다. 이 부분은 마우스를 처리할 때 알아보기로 하고, 일단은 초기화하는 방법을 알아봅시다.

    public override string? Execute(IPlayer player)
    {
        var id = base.Execute(player);
    
        if (player.Manager.SerialPort != null && player.Manager.SerialPort.IsOpen)
            player.Manager.Output.WarningWriteLine(string.Format(player.Manager.Client.ResxMessage.GetString("SerialConnectionOpend"), SerialPortName));
        else
        {
            var sp = new System.IO.Ports.SerialPort();
            sp.PortName = SerialPortName;
            sp.DtrEnable = DtrEnable;
            sp.RtsEnable = RtsEnable;
            sp.BaudRate = BaudRate;
            sp.DataBits = DataBits;
            sp.Parity = Parity;
            sp.StopBits = StopBits;
            sp.ReadTimeout = ReadTimeout;
            sp.WriteTimeout = WriteTimeout;
    
            player.Manager.SerialPort = sp;
            player.Manager.SerialPort.Open();
            player.Manager.Output.WarningWriteLine(string.Format(player.Manager.Client.ResxMessage.GetString("SerialConnectionComplete"), SerialPortName));
        }
    
        player.Manager.SerialMouseSpeed = Speed;
        player.Manager.SerialMouseDistance = Distance;
        player.Manager.SerialAutoCorrection = AutoCorrection;
    
        return id;
    }

     

    시리얼포트를 초기화하는 부분과 실제 운영상 필요한 속성 부분이 분리되어 있습니다. 만약, 시리얼포트를 변경해야 한다면 어쩔 수 없이 Disconnection 모델을 실행하고 다시 연결해야 변경할 수 있습니다. 하지만, 생성자와 관련이 없는 운영용 속성들은 아무때나 변경해도 상관 없습니다.

     

    시리얼포트 설정은 글로벌하게 마우스와 키보드에 적용되어야 합니다. 그리고, 장치이기 때문에 플레이어별로 여러개를 셋팅할 수 없기도 합니다. 그렇기 때문에 싱글톤으로 구현된 매크로 매니저에서 내용을 보관하고 있다가 모든 플레이어가 정보를 사용할 수 있도록 해야 합니다. 아래는 매니저의 인터페이스입니다.

    int SerialMouseSpeed { get; set; }
    
    int SerialMouseDistance { get; set; }
    
    bool SerialAutoCorrection { get; set; }

     

    아두이노 업로드를 위해 유틸리티 메뉴를 하나 추가했습니다.

    j1Gwf4d.jpeg

     

     

    에디터의 유틸리티 메뉴를 초기화할 수 있는 메소드를 하나 만들었습니다.

    EditorUtilityMenu((Ai.Interface.IEditor)_client);

     

    크립톤 리본 메뉴에 그룹 목록을 만들고, 그룹안에 버튼을 배치합니다. 만들어진 목록과 오브젝트를 상위 컨테이너에 추가해주면 메뉴가 생성됩니다.

    private void EditorUtilityMenu(Ai.Interface.IEditor editor)
    {
        var utilityGroup = new ComponentFactory.Krypton.Ribbon.KryptonRibbonGroup();
        utilityGroup.TextLine1 = "Utility";
        var groupTriple = new ComponentFactory.Krypton.Ribbon.KryptonRibbonGroupTriple();
        var arduinoGroupButton = new ComponentFactory.Krypton.Ribbon.KryptonRibbonGroupButton();
        arduinoGroupButton.TextLine1 = "Arduino";
        arduinoGroupButton.TextLine2 = "Upload";
        arduinoGroupButton.Click += ArduinoGroupButton_Click;
    
        groupTriple.Items.AddRange(new ComponentFactory.Krypton.Ribbon.KryptonRibbonGroupItem[] {
            arduinoGroupButton
        });
    
        utilityGroup.Items.AddRange(new ComponentFactory.Krypton.Ribbon.KryptonRibbonGroupContainer[] {
            groupTriple
        });
    
        editor.Ribbon.RibbonTabs[5].Groups.Add(utilityGroup);
    }

     

    제 컴퓨터에 연결되어 있는 장치들의 콤포트가 목록으로 표시됩니다.

    XK1PxgD.jpeg

     

     

    아두이노가 연결된 콤포트를 선택하고, OK를 누르면 장치로 아두이노 코드가 업로드되고, 컴파일됩니다. 이 작업은 약 30~60초정도 걸립니다.

    rw9jk9Z.jpeg

     

     

    이제 하드웨어 마우스 매크로를 거의 다 만들었습니다. 마우스 클릭 부분에서 몇가지 코딩만 해주면 됩니다. 먼저 아래와 같이 잘못된 정보가 들어오는걸 방지하기 위한 방어코드를 만들어줍니다. 마우스 속도는 1부터 100까지만 입력할 수 있는데요. 사용자가 101을 입력하면 100으로 변경한 후 마우스 클릭이 동작하도록 합니다.

    int speed = 101 - player.Manager.SerialMouseSpeed;
    
    if (speed < 1)
        speed = 1;
    
    if (speed > 100)
        speed = 100;
    
    int distance = player.Manager.SerialMouseDistance;
    
    if (distance < 1)
        distance = 1;
    
    if (distance > 100) 
        distance = 100;

     

    위의 Distance는 거리를 의미합니다. 마우스가 움직이는 속도는 스피드로 조정할 수 있지만, 위에서 언급했듯이 마우스가 이동할 수 있는 최대 거리는 127입니다. 하지만, 너무나 빈약한 성능을 가진 아두이노 특성상 127까지 모든 성능을 뽑아내는건 거의 불가능합니다. 그래서, 어느정도 조정이 필요한데요. 실제로 127까지 입력할 수 있더라도 최대 값은 100으로 리밋을 설정해두었습니다.

     

    만약, 저 리밋을 풀면... 엄청난 질문 폭탄을 받게 될겁니다. 이런저런 문제들 때문에 말이죠. 엔지엠 6에서는 컨셉이 자유였습니다. 그래서, 사용자의 환경과 성능에 맞게 스스로 값을 조정해서 사용하도록 했습니다. 하지만, 개발자 마인드를 가진 제가 예상하지 못한것들이 있었는데요. 대부분의 사용자는 쉽고 편리하게 쓰길 원한다는 것입니다. 옵션이 많으면 혼란만 가중시킨다는걸 몰랐던거죠. 생각해보면 저 또한 마찬가지입니다. 프로그래밍쪽은 제 밥벌이니깐 디테일한것에 의미를 부여하고 자유가 무조건 좋은것인줄 알았습니다.

     

    저도 다른 유료 제품들을 사용하다보면 왜 제약이 있어야 하는지에 대해서 항상 불만이 있었거든요. 그런데, 제가 잘 모르는 분야인 자전거나 등산 장비 또는 자동차와 같은 것들을 보면 너무나 많은 옵션과 성능 차이 때문에 선택 장애가 오곤 합니다. 막상 물건을 구매해도 최상 옵션이 아니면... 왠지 찝찝하기도 하고 더 좋은 가성비 옵션이었는지 또 확인하고 그럽니다. 모두가 비슷할거라고 생각합니다. 그래서, 새로운 엔지엠 매크로에서는 안정적으로 돌아갈 수 있도록 리밋을 두기로 했습니다.

     

    이제 마우스 동작을 구현해야 하는데요. 현재 위치에서 목표 지점까지 몇번의 스텝으로 이동할지를 계산해줘야 합니다. 이 때 중요한점은 위에서도 계속 설명했듯이 127 안쪽으로 마우스가 이동하도록 제약을 걸어야 한다는 것입니다. 그래서, 시작점과 종료점을 이동할 때 몇번의 스텝으로 가야하는지를 계산해줘야 합니다.

    public static List<Point> LinearSmoothMove(Point start, Point end, int steps)
    {
        List<Point> points = new List<Point>();
        PointF iterPoint = start;
        PointF slope = new PointF(end.X - start.X, end.Y - start.Y);
        slope.X = slope.X / steps;
        slope.Y = slope.Y / steps;
    
        for (int i = 0; i < steps; i++)
        {
            iterPoint = new PointF(iterPoint.X + slope.X, iterPoint.Y + slope.Y);
            points.Add(Point.Round(iterPoint));
        }
    
        if (!points.Contains(end))
            points.Add(end);
    
        return points;
    }

     

    쉽게 예를 들어서 마우스 이동 제약이 10이라고 할 때 시작이 0이고 마지막이 100이면 스텝은 10이됩니다. 10씩 10번 이동하면 목표 지점에 도달하니까요. 이런식으로 계산을 해야 하는데요. 이 때 상하좌우에 따라서 대각선으로 이동하도록 해야하기 때문에 몇가지 계산 로직이 더 필요해집니다.

     

    디바이스 인풋 타입이 시리얼인 경우 아래와 같이 처리해줍니다.

    case Ai.Definition.DeviceInputType.Serial:
        serialPort?.Write("1");
        Task.Delay(serialMouseSpeed.Value).Wait();
        serialPort?.Write("2");
        break;

     

    1은 마우스 다운이고, 2는 마우스 업입니다. 이 둘이 조합되면 마우스 클릭이 됩니다. 테스트를 위해 그림판을 하나 열고, 타점을 만들어줍니다.

    ZSPzWtf.jpeg

     

     

    아래 동영상을 참고해서 설정 정보를 확인하고, 매크로를 실행 해보세요. 마우스가 이동하면서 좌상단의 동그라미 영역 안쪽을 클릭합니다.

     

     

    이번에는 마우스 이동을 사용하지 않음으로 변경하고, 실행 해볼께요. 엔지엠 6에서는 지원하지 않았던 기능인데요. 새롭게 만드는 엔지엠 7 버전에서는 하드웨어 방식을 사용하더라도 마우스 클릭이 동작하도록 구현되어 있습니다. 좀 더 빠르게 작업할 수 있어서 좋긴하지만, 중간 단계를 모두 건너뛰기 때문에 문제가 될수도 있습니다. 자동화를 감지하는 여러가지 제품들도 있기 때문인데요. 이런 위험을 피하려면 정상적인 신호를 사용하는게 좋을듯 합니다.

     

     

    이렇게해서 마우스 클릭을 하드웨어 또는 기계식으로 입력하는 방법을 알아봤습니다. 다음 2부에서는 키보드도 하드웨어 방식으로 입력할 수 있게 만들어 볼께요. 이외에도 에디터의 메뉴들을 개발하느라 시간을 많이 소비했는데요. 요즘은 이것저것 처리할게 많아서 작업 속도가 점점 느려지고 있습니다. 근래에 잘 걸리지도 않던 감기에 걸려서 조금 많이 힘들기도 했고요. 월요일부터는 다시 화이팅하고~ 새로운 마음으로 출발해보도록 하겠습니다.

     

    개발자에게 후원하기

    MGtdv7r.png

     

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

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

    감사합니다~

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

    댓글목록

    등록된 댓글이 없습니다.