NGMsoftware

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

    학습


    C# C# .NET 매크로 프로그램 만들기. (텍스트 파일 또는 엑셀 파일 반복 매크로 만들기 1부)

    페이지 정보

    본문

    안녕하세요. 엔지엠소프트웨어입니다. 반복기 액션들에 대해서 알아보도록 하겠습니다. 우선, 반복기란 For 나 Foreach 또는 While, Do~While문 등등이 있습니다. 엔지엠 매크로에서는 액션 이동, 액션 반복과 포 반복, 포이치 반복등등이 존재합니다. 반복기는 정해진 횟수만큼 반복하거나 특정 조건이 될 때까지 반복하는 방식으로 구현하는데요. 액션 이동 관련해서 이미 만들었기 때문에 여기에서는 포 반복과 포이치 반복에 대해서 만들어볼께요.

     

    우선, 포 반복은 사용자가 횟수를 정하면 그 횟수만큼 액션이 반복되는걸 말합니다. 포이치 반복도 포 반복과 비슷하지만, 포이치 반복은 주어진 파일 또는 배열과 같은 오브젝트의 항목 수만큼 자동으로 반복합니다. 약간의 차이가 있긴한데요. 개발자가 아닌 경우 글의 설명만으로는 이해하기가 어려울 수 있습니다. 예제와 테스트를 보면 아마도 쉽게 이해할 수 있을거예요.

     

    포 반복 액션을 먼저 만들건데요. 이 액션은 함수 상자쪽에 추가할겁니다. 아래와 같이 모델 클래스를 하나 만들어줍니다.

    lzrCc5f.jpeg

     

     

    트리 메뉴에 아래와 같이 모델을 추가하고, 아이콘도 달아줍니다.

    #region Function
    var function = _treeView.Nodes[Ai.FunctionCategory.Function];
    if (function != null)
    {
        function.Nodes
            .Add(Ai.Function.Group, _resxCaption.GetString(Ai.Function.Group), "group", "group")
            .Tag = new Ai.Model.Action.Function.GroupModel() { DisplayText = _resxCaption.GetString(Ai.Function.Group) };
    
        function.Nodes
            .Add(Ai.Function.Description, _resxCaption.GetString(Ai.Function.Description), "description", "description")
            .Tag = new Ai.Model.Action.Function.DescriptionModel() { DisplayText = _resxCaption.GetString(Ai.Function.Description) };
    
        function.Nodes
            .Add(Ai.Function.Goto, _resxCaption.GetString(Ai.Function.Goto), "goto", "goto")
            .Tag = new Ai.Model.Action.Function.GotoModel() { DisplayText = _resxCaption.GetString(Ai.Function.Goto) };
    
        function.Nodes
            .Add(Ai.Function.GotoIndex, _resxCaption.GetString(Ai.Function.GotoIndex), "goto_index", "goto_index")
            .Tag = new Ai.Model.Action.Function.GotoIndexModel() { DisplayText = _resxCaption.GetString(Ai.Function.GotoIndex) };
    
        function.Nodes
            .Add(Ai.Function.For, _resxCaption.GetString(Ai.Function.For), "for", "for")
            .Tag = new Ai.Model.Action.Function.ForModel() { DisplayText = _resxCaption.GetString(Ai.Function.For) };

     

    메뉴 구성, 트리 구성, 아이콘 구성은 이미 알아본 내용이라서 자세하게 설명하지는 않고, 대략 코드 스타일만 보고 넘어갈께요. 아래는 아이콘 처리 부분이예요.

    #region Function
    imageList.Images.Add("func", (Image)_resxImage.GetObject("func"));
    imageList.Images.Add("group", (Image)_resxImage.GetObject("group"));
    imageList.Images.Add("description", (Image)_resxImage.GetObject("description"));
    imageList.Images.Add("goto", (Image)_resxImage.GetObject("_goto"));
    imageList.Images.Add("goto_index", (Image)_resxImage.GetObject("goto_index"));
    imageList.Images.Add("for", (Image)_resxImage.GetObject("_for"));

     

    포 반복 모델은BaseModel을 상속 받고, IIndependentModel과 IRepeatModel 인터페이스 2개도 상속 받아줍니다.

    public class ForModel : BaseModel, Ai.Interface.IIndependentModel, Ai.Interface.IRepeatModel

     

    인디펜던트는 독립이라는 뜻입니다. 매크로 프로그램 환경에서 독립이란, 자신을 실행하는 스레드(또는 태스크)와 독립적으로 실행되는 모든 것을 의미합니다. 대표적으로 그룹 액션이 있는데요. 그룹 액션도 속성으로 비동기 사용이 있습니다. 비동기를 사용하면 자신이 속한 스크립트와는 별개로 그룹 안에 포함된 액션들이 독립적으로 동작하게 됩니다. 만약, 엔지엠 6을 사용해보신 분들은 이벤트 액션들을 보셨을겁니다. 또는 사용 해보신 분들도 있을텐데요. 이와 비슷하다고 보면 됩니다. 다만, 자유도면에서 엔지엠 6의 이벤트와는 비교할 수 없을정도로 사용자가 마음대로 편집해서 사용할 수 있습니다.

    public interface IIndependentModel
    {
        bool UseAsynchronous { get; set; }
    }

     

    IRepeatModel은 반복기 액션들이 모두 상속 받아서 구현해야 하는 강제 속성입니다. 반복하면서, 현재 어디까지 반복했는지를 표시하기 위한 인터페이스입니다.

    public interface IRepeatModel
    {
        int CurrentRepeatCount { get; set; }
    }

     

    인터페이스에 대해서 개발자라면 모두 알고 있을텐데요. 일반인분이 개발을 배우거나 매크로 프로그램에 관심이 있어서 이 글을 보신다면 이게 어떤 의미인지 이해하기가 어려울겁니다. 인터페이스란 서로 다른 모델(객체: Object)들이 하나의 기능을 공유할 때 우리는 이 기능을 사용하려면 이런 인터페이스를 사용하자는 일종의 약속과 같습니다. 계산기를 만드는 제조사가 A, B, C, D...등등... 무수히 많을겁니다.

     

    이들이 모두 모양이 다른 키패드를 사용한다면 계산 로직을 만드는 핵심 모듈에서는 각각의 형식에 맞는 모든 처리를 만들어야 할겁니다. 이렇게하면 엄청난 자원의 낭비가 발생하고, 관리하기도 쉽지 않을겁니다. 그래서, 계산기의 중앙 가장 아래쪽은 0이고, 좌측 상단부터 1이 시작해서 우측 하단은 9로 마무리 되도록 약속한겁니다. 엔지엠 매크로 입장에서 보면, 중앙에 모델 액션을 처리하는 장치가 하나 있습니다. 이 장치는 들어오는 모델의 타입을 분석하고, 이 모델이 IRepeat를 상속 받고 있다면 현재 반복중인 횟수를 입력해줍니다.

     

    인터페이스에 대해서 좀 더 쉽게 설명할 수 있으면 좋겠는데요. 이게 말로 설명하는것과 글로 설명하는게 차이가 있다보니 어디서부터 어떻게 설명을 해야 할지 참 어렵네요^^;

     

    인터페이스를 구현하는 속성은 직렬화하지 않도록 해야 합니다. 속성은 NonSerialized 어트리뷰트를 설정할 수 없기 때문에 프라이빗 맴버를 하나 만들고, 속성에서 읽고 쓸 수 있도록 구성해야 합니다. 그리고, 비동기로 반복할 수 있도록 처리했습니다.

    [NonSerialized]
    private int _currentRepeatCount = 0;
    
    [LocalizedCategory("Data")]
    [LocalizedDisplayName("CurrentRepeatCount")]
    [LocalizedDescription("CurrentRepeatCount")]
    [Browsable(true)]
    [ReadOnly(true)]
    [DefaultValue(0)]
    public int CurrentRepeatCount { get { return _currentRepeatCount; } set { _currentRepeatCount = value; } }
    
    [LocalizedCategory("Action")]
    [LocalizedDisplayName("UseAsynchronous")]
    [LocalizedDescription("UseAsynchronous")]
    [Browsable(true)]
    [DefaultValue(false)]
    public bool UseAsynchronous { get; set; }
    
    [LocalizedCategory("Action")]
    [LocalizedDisplayName("CompareRepeat")]
    [LocalizedDescription("RepeatCount")]
    [Browsable(true)]
    [DefaultValue(1)]
    public int RepeatCount { get; set; } = 1;

     

    비동기는 별도의 스레드(Thread, 일반적으로 Task를 사용)로 동작할 수 있는 방법을 제공합니다. 이 부분이 상당히 복잡하고, 어려운 내용인데요. 가능하면 간략하게만 설명하고 넘어갈께요. 비동기 처리를 유기적으로 결합해서 다른 스레드에서 동작중인 스크립트와 상호작용하려면 많은 부분들이 수정되어야 합니다. 스레드끼리 값을 주고 받는 부분과 이 부분이 사라지지 않게 동기화하는 작업도 만들기 어려운데다가 테스트도 까다롭습니다.

     

    Execute 실행 부분의 코드는 간단합니다.

    public override string? Execute(IPlayer player)
    {
        var id = base.Execute(player);
    
        if (UseAsynchronous && !Ai.Common.Helper.NullCheckAndWriteLine(player, nameof(ID), ID))
            return null;
    
        if (UseAsynchronous && Actions?.Count > 0)
            player.SubTask(ID, player.GetActionList(Actions.Cast<IModel>().ToList()), Ai.Definition.ActionType.For, RepeatCount, this);
    
        return id;
    }

     

    반복기를 만들면서 비동기를 처리 하려다보니 디자인이 상당히 복잡해졌습니다. 일단 매인 스크립트가 있고, 매인 스크립트 안에 포 반복이 비동기로 동작한다고 할 때 이 태스크를 중지하려면 매인 스크립트가 태스크를 소유하고 있어야 합니다. 태스크는 소유자가 없으면 중지시킬 수 없습니다. 물론, 태스크가 무한 반복이 아니라면 언젠가는 스스로 중지하고 메모리를 비울겁니다.

     

    하지만, 로직적으로 뭔가를 체크할 때까지 반복한다면 문제가 될 수 있습니다. 그래서, 위와 같이 스크립트가 실행중인 플레이어(Player)의 서브 태스크로 포 반복을 등록해줘야 합니다. 그래야, 스크립트의 상태에 따라서 비동기적으로 실행중인 모든 태스크를 관리할 수 있기 때문입니다.

     

    일단, IPlayer 인터페이스에 메소드를 정의합니다.

    void SubTask(string funcId, List<IModel> actions, Ai.Definition.ActionType actionType, int repeatCount = 1, IModel? taskCallAction = null);

     

    Player 클래스에서 메소드를 구현합니다.

    public void SubTask(string taskId, List<IModel> actions, Ai.Definition.ActionType actionType, int repeatCount = 1, IModel? taskCallAction = null)
    {
        CancellationTokenSource cts = new CancellationTokenSource();
        CancellationToken ct = cts.Token;
        Run(taskId, actions.Cast<ActionModel>().ToList(), actionType, repeatCount, cts, ct, taskCallAction as ActionModel);
    }

     

    Run 메소드가 실제로 테스크를 생성하고, 매크로를 동작시키는 부분입니다.

    _taskList.Add(scriptId, new TaskInfo(Task.Run(() =>
    {
        Task currentTask = _taskList[scriptId].Task;
        TreeNode? beforeNode = null;
    
        if (!Script.IsBackground)
            backColor = actions.First().TreeNode?.BackColor;
    
        Ai.Interface.IEditor? editor = null;
        if (Manager.Client is Ai.Interface.IEditor)
            editor = (Ai.Interface.IEditor)Manager.Client;
    
        TaskRun(scriptId, editor, currentTask, actions, beforeNode, actionType, repeatCount, cts, ct, taskCallAction);

     

    테스크에서 넘어온 모델이 포 반복이라면 별도로 Execute를 처리해줍니다. 서브 태스크로 넘겨서 처리하기 위함입니다.

    if (action is Ai.Model.Action.Function.ForModel)
    {
        var forAction = (Ai.Model.Action.Function.ForModel)action;
        forAction.CurrentRepeatCount = 0;
    
        if (forAction.UseAsynchronous)
        {
            if (Manager.Output.ShowDebug)
                Manager.Stopwatch.Restart();
                forAction.Execute(this);

     

    만약, 비동기가 아니라면 현재 태스크에서 자식 액션들을 모두 반복해줍니다. TaskRun은 새로 생성된 스레드가 호출할수도 있고, 아래와 같이 직접 호출할수도 있습니다. 태스크가 만들어지면 독립적으로 수행하지만, 아래와 같이 메소드로 호출하면 해당 메소드가 완료될 때까지 다른 구문으로 넘어가지 않습니다. 만약, 디자인적으로 async, await를 사용했다면, 테스크런 메소드를 비동기로 만들어야 합니다.

    if (forAction.Actions != null && forAction.Actions.Count > 0)
    {
        var models = GetActionList(forAction.Actions.Cast<IModel>().ToList()).Cast<ActionModel>().ToList();
        TaskRun(taskId, editor, currentTask, models, beforeNode, Ai.Definition.ActionType.For, forAction.RepeatCount, taskCallAction: forAction);

     

    테스크런 메소드의 마지막에 자신을 실행한 모델이 IRepeatModel을 구현하고 있는지 체크합니다. 구현하고 있다면 현재 반복한 횟수를 증가시켜줍니다. 여기서 인터페이스를 사용하는 이유가 나옵니다. 태스크런에는 수많은 액션들이 있을텐데요. IRepeatModel을 구현한 액션은 모두 CurrentRepeatCount 속성을 가지고 있을겁니다. 인터페이스는 강제성을 띄고 있어서 무조건 구현해야 하기 때문입니다.

    if (taskCallAction != null)
    {
        if (taskCallAction is Ai.Interface.IRepeatModel)
            ((Ai.Interface.IRepeatModel)taskCallAction).CurrentRepeatCount++;
    
        SetVariableProcess(taskCallAction);
        Manager.Output.WriteLine(taskCallAction);
    }

     

    이제 에디터를 실행하고, 새로운 스크립트를 아래와 같이 구성 해볼께요. 반복 횟수는 3으로 설정했습니다.

    s2WEHti.jpeg

     

     

    로그는 아래와 같이 3회 반복한걸로 나오네요.

    [[NEW] Script 1] 스크립트를 실행했습니다.
    포 반복
    [[NEW] Script 1] 포 반복을 실행했습니다.
    그룹
    00:00:00.000
    반복한 횟수:1
    
    그룹
    00:00:00.000
    반복한 횟수:2
    
    그룹
    00:00:00.000
    반복한 횟수:3
    
    [[NEW] Script 1] 스크립트가 완료되었습니다.

     

    매크로를 실행하고, 결과를 확인해보면 반복기에서 설정한 횟수만큼 동작하는걸 알 수 있습니다.

     

     

    이번에는 조건에 따라서 반복하는 도중에 빠져 나가는 방법을 알아볼께요. 아래와 같이 변수를 하나 추가하고, 포 반복의 반복한 횟수 속성을 추가해줍니다.

    8NT1WEO.jpeg

     

     

    그리고, 숫자 체크 조건을 추가한 후 반복한 횟수가 2와 같은지 비교하고, 마지막 그룹으로 빠져나가도록 만들었습니다.

    CAyKyXq.jpeg

     

     

    포 반복기에서 조건이 정상적으로 처리되는지 아래 동영상과 같이 테스트 해보세요.

     

     

    개발자에게 후원하기

    MGtdv7r.png

     

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

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

    감사합니다~

     

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

    댓글목록

    등록된 댓글이 없습니다.