[C#] 윈도우에서 컨트롤 동기화하는 방법(InvalidOperationException 에러)


Development note/C#  2021. 1. 14. 19:33

안녕하세요. 명월입니다.


이 글은 C#의 윈도우에서 컨트롤 동기화하는 방법(InvalidOperationException 에러)에 대한 글입니다.


C#를 사용하게 된다면 MVC를 통해서 웹 환경을 만드는 경우도 있으나 보통 Window 폼이나 콘솔 배치를 만드는 일이 많을 것입니다. 아무래도 웹은 C#으로 작성하기에는 아무래도 단가가 비싸니깐 Java로 만드는 경우가 많겠네요.

개인적으로 CI툴(Jenkins)등 웹 개발에 도움을 주는 툴이 보통 Java에 많이 포커스가 맞추어져 있는 경향이 있어서 C#으로는 윈도우 툴을 만드는 데 많이 사용할 것입니다.


C#으로 윈도우 개발을 한다면 가장 많은 수요는 아무래도 게임일 테고.. 저도 아직까지 써보지는 않았지만 Unity 엔진을 사용하고 Unity 엔진에서 스크립트로 C#를 사용한다고 들었습니다. 시간나면 공부해보고 싶은 생각이 매우 많습니다만 아직 그럴 시간이 없네요.

게임 이외로 윈도우 툴을 만드는 경우가 많습니다.


C/C++에서는 윈도우 메시지 처리 함수가 있어서 거기에서 데이터를 넣고 동기화를 하면 크게 문제없이 되었습니다.

C#에도 윈도우 메시지 처리하는 함수가 있습니다.

using System;
using System.Windows.Forms;

namespace WindowForm
{
  // Form 클래스를 상속받는다.	
  public class Program : Form
  {
    public Program()
    {
    }
    // 윈도우 메시지 처리 함수
    protected override void WndProc(ref Message m)
    {
      base.WndProc(ref m);
      // 메시지 콘솔 출력
      Console.WriteLine(m.Msg);
    }

    // 싱글 스레드 어트리뷰트	
    [STAThread]
    // 실행 함수	
    static void Main(string[] args)
    {
      // 환경에 맞는 스타일 설정	
      Application.EnableVisualStyles();
      Application.SetCompatibleTextRenderingDefault(false);
      // 메시지 루프에 인스턴스를 생성한다.	
      Application.Run(new Program());
    }
  }
}

일단 저는 설명하기 편하게 Console환경에서 윈도우 폼을 작성하겠습니다. 보고 따라하시는 분들은 시작을 윈도우 프로젝트에서 시작하셔도 됩니다.

위에 예제처럼 WndProc함수가 있어서 C/C++처럼 메시지 처리가 가능합니다. 그런데 제가 텍스트 박스를 만들고 서브 스레드를 생성시켜 데이터를 넘겨보겠습니다.

using System;
using System.Drawing;
using System.Threading;
using System.Windows.Forms;

namespace WindowForm
{
  // 텍스트 박스
  public class CustomTextBox : TextBox
  {
    // 생성자
    public CustomTextBox(string name, Size size)
    {
      // 컨트롤 키 설정
      this.Name = name;
      // 멀티 라인 허용
      this.Multiline = true;
      // 글자 크기 설정
      this.Font = new Font(new FontFamily("Arial"), 30);
      // 컨트롤 크기 설정
      this.Size = new Size(size.Width - 16, size.Height - 40);
    }
  }
  // Form 클래스를 상속받는다.	
  public class Program : Form
  {
    public Program()
    {
      this.Controls.Add(new CustomTextBox("test", this.Size));
      // 스레드 생성
      ThreadPool.QueueUserWorkItem((_) =>
      {
        // 무한 루프
        while (true)
        {
          // test 키를 가진 컨트롤 텍스트에 현재 시간을 넣는다.
          this.Controls["test"].Text = DateTime.Now.ToString("hh:mm:ss");
          // 1초 대기
          Thread.Sleep(1000);
        }
      });
    }
    // 윈도우 메시지 처리 함수
    protected override void WndProc(ref Message m)
    {
      base.WndProc(ref m);
      // 메시지 콘솔 출력
      Console.WriteLine(m.Msg);
    }

    // 싱글 스레드 어트리뷰트	
    [STAThread]
    // 실행 함수	
    static void Main(string[] args)
    {
      // 환경에 맞는 스타일 설정	
      Application.EnableVisualStyles();
      Application.SetCompatibleTextRenderingDefault(false);
      // 메시지 루프에 인스턴스를 생성한다.	
      Application.Run(new Program());
    }
  }
}

위 Program 생성자 안에 스레드가 생성되고 1초마다 텍스트 박스에다가 현재 시간을 넣는 프로그램을 만들었습니다.

실행을 하게 되면 아래와 같은 에러가 발생합니다.

Visual studio는 영문판인데 뜬금없이 일본어가 나와서 당황스럽네요..

대충 내용은 위의 스레드가 Window 메시지의 스레드(?)와 동기화가 되지 않아서 발생하는 에러입니다. 예전 C나 C++은 SendMessage 함수나 PostMessage 함수가 있어서 자연스럽게 Window 메시지로 넣을 수 있는데, 여긴 방법이 없네요.

전혀 없지는 않습니다. 변수를 만들어서 거기에 값을 넣고 WndProc로 메시지를 날린다음 TextBox에서 다시 WndProc로 메시지를 받은 다음 Text를 수정하는 방법으로는 가능합니다. 무지하게 복잡하네요..


쉽게 하는 방법을 소개 하겠습니다.

using System;
using System.Drawing;
using System.Threading;
using System.Windows.Forms;

namespace WindowForm
{
  // 컨트롤 확장
  public static class ExtendControl
  {
    // 동기화 함수 추가
    public static void InvokeControl(this Control ctl, Action func)
    {
      // 메시지가 실행 중이면
      if (ctl.InvokeRequired)
      {
        // Invoke 함수로 동기화 한다.
        ctl.Invoke(func);
      }
      else
      {
        // 그냥 함수 호출
        func();
      }
    }
    // 동기화 함수 추가 (Return이 가능하다.)
    public static T InvokeControl<T>(this Control ctl, Func<T> func)
    {
      // 메시지가 실행 중이면
      if (ctl.InvokeRequired)
      {
        // Invoke 함수로 동기화 한다.
        return (T)ctl.Invoke(func);
      }
      else
      {
        // 그냥 함수 호출
        return func();
      }
    }
  }
  // 텍스트 박스
  public class CustomTextBox : TextBox
  {
    // 생성자
    public CustomTextBox(string name, Size size)
    {
      // 컨트롤 키 설정
      this.Name = name;
      // 멀티 라인 허용
      this.Multiline = true;
      // 글자 크기 설정
      this.Font = new Font(new FontFamily("Arial"), 30);
      // 컨트롤 크기 설정
      this.Size = new Size(size.Width - 16, size.Height - 40);
    }
  }
  // Form 클래스를 상속받는다.	
  public class Program : Form
  {
    public Program()
    {
      this.Controls.Add(new CustomTextBox("test", this.Size));
      // 스레드 생성
      ThreadPool.QueueUserWorkItem((_) =>
      {
        // 무한 루프
        while (true)
        {
          // 동기화를 합니다.
          this.Controls["test"].InvokeControl(() =>
          {
            // test 키를 가진 컨트롤 텍스트에 현재 시간을 넣는다.
            this.Controls["test"].Text = DateTime.Now.ToString("hh:mm:ss");
          });
          // 1초 대기
          Thread.Sleep(1000);
        }
      });
    }
    // 윈도우 메시지 처리 함수
    protected override void WndProc(ref Message m)
    {
      base.WndProc(ref m);
      // 메시지 콘솔 출력
      Console.WriteLine(m.Msg);
    }

    // 싱글 스레드 어트리뷰트	
    [STAThread]
    // 실행 함수	
    static void Main(string[] args)
    {
      // 환경에 맞는 스타일 설정	
      Application.EnableVisualStyles();
      Application.SetCompatibleTextRenderingDefault(false);
      // 메시지 루프에 인스턴스를 생성한다.	
      Application.Run(new Program());
    }
  }
}

위에 확장 클래스를 만들어서 InvokeControl함수를 추가합니다.

Form과 각종 컨트롤은 모두 Control 클래스를 상속 받기 때문에, Control 컨트롤에 확장 함수를 걸어 놓으면 모든 컨트롤 객체에 적용이 됩니다. (MS는 이런거 만들어 놓지 왜 귀찮게 Window 개발할 때마다 동기화 프로세스를 넣어야 하는 걸까?)

그리고 Stack 영역 안에서는 동기화가 이루어 집니다. 즉, 텍스트 박스에 동기화를 걸고 동기화가 되면 Text의 현재 시간을 넣는 식으로 처리됩니다.


실행하면 1초마다 텍스트 박스에 현재 시간이 바뀌는 것을 확인할 수 있습니다.


여기까지 C#의 윈도우에서 컨트롤 동기화하는 방법(InvalidOperationException 에러)에 대한 글이었습니다.


궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.