[C#] 57. 윈도우 폼(Window form)을 작성하는 방법, 그리고 윈도우 메시지와 큐


Study/C#  2021. 10. 27. 20:27

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


이 글은 C#에서 윈도우 폼(Window form)을 작성하는 방법, 그리고 윈도우 메시지와 큐에 대한 글입니다.


C#이란 언어는 MS사에서 개발한 언어로써 Window OS에 특화되어 있습니다. 즉, C# 언어로 우리가 사용하는 윈도우 환경에서 윈도우 프로그램을 개발할 수 있습니다.

Java나 그밖에 python등을 통해서도 윈도우 프로그램을 못 만드는 것은 아니지만, 아무래도 윈도우가 MS사의 제품이기 때문에 C#에서 Window API를 가져다 쓰는게 더 쉽습니다.

사실 C# 언어를 배우는 것이 거의 윈도우 프로그램을 만들기 위한 것이라고 해도 틀리지 않습니다. 요즘 게임 엔진으로 많이 사용되는 Unity도 기본적으로 사용되는 언어는 C#입니다.


그럼 간단하게 윈도우 폼을 작성해 보도록 하겠습니다.

Visual studio를 켜서 프로젝트 작성를 실행합니다.

그럼 Window Forms App 항목이 두개가 나옵니다.

하나는 Core 용이고, 하나는 .Net framework 용입니다. Core을 사용해도 상관은 없습니다만, Window form은 기본적으로 Window에서 실행하는 프로그램인데 굳이, Core를 선택할 필요는 없습니다.

최근에 사용하는 Window 10에는 기본적으로 .Net framework가 설치 되어 있으니깐요.


그리고 프로젝트 명을 입력하고 프로젝트 작성을 합니다.

그럼 Visual studio에 기본적으로 Window form이 생성되어 있습니다.

그대로 F5(Starting debugging)을 누르면 윈도우에 윈도우 프로그램이 실행되어 있는 것을 확인할 수 있습니다.

사실 여기까지만 해도 윈도우 프로그램 하나를 만든 것과 같습니다.


여기서 우리가 윈도우 폼에 버튼을 하나 추가 해보도록 합시다.

Visual studio에 옆을 보면 Toolbox가 있는 것을 확인할 수 있습니다.

거기서 Button을 찾아서 폼으로 마우스로 끌어다가 놓습니다.(Drag-and-drop)

그럼 폼에 버튼이 생긴 것을 확인할 수 있습니다.


그럼 버튼을 더블 클릭을 합니다.

그러면 소스 화면으로 바뀌는 것을 확인할 수 있습니다.

무언지는 모르겠지만 button1_Click이라는 함수가 있네요. 여기에 클릭을 할 때 동작하는 소스을 넣는 것 같습니다.

using System;
using System.Windows.Forms;

namespace WindowsFormsApp
{
  public partial class Form1 : Form
  {
    // 폼 생성자
    public Form1()
    {
      InitializeComponent();
    }
    // 버튼 클릭 이벤트
    private void button1_Click(object sender, EventArgs e)
    {
      // 메시지 박스
      MessageBox.Show("Hello world");
    }
  }
}

button1_Click 함수 안에 MessageBox.Show("Hello world")를 입력하고 다시 F5(Starting debugging)를 누르고 디버깅을 합시다.


실행을 하면 아까처럼 윈도우 프로그램이 생성이 되고 그 안에 버튼이 있는 것을 확인할 수 있습니다. 버튼을 클릭하면 메시지 박스에 Hello world라는 문자가 보이는 것을 확인할 수 있습니다.

여기까지가 기본적인 윈도우 프로그램 생성 흐름입니다.


소스에 대해서 조금 자세히 설명하겠습니다.

옆의 탐색기를 보면 Form1.cs파일이 있고, Form1.Desiner.cs파일과 Form1.resx 파일이 있습니다.

그리고 Program.cs 파일이 있네요.

먼저 Program.cs 파일을 열어보면 아래와 같은 소스가 있습니다.

using System;
using System.Windows.Forms;

namespace WindowsFormsApp
{
  static class Program
  {
    // 단일 스레드 아파트 먼트(Single-threaded apartment) 어노테이션 설정
    [STAThread]
    // 실행함수
    static void Main()
    {
      // 이건 Visual 스타일의 설정하는 것, 즉 설정하지 않으면 조금 옛날 분위기의 윈도우 프로그램이 생성
      Application.EnableVisualStyles();
      // 이건 Text 랜더링에 대한 설정인데, 문자에 대한 간격 설정과 기타 등등의 설정, Default는 false로 설정
      Application.SetCompatibleTextRenderingDefault(false);
      // 윈도우 프로그램의 메시지를 돌리는 함수
      Application.Run(new Form1());
    }
  }
}

기본적으로 위 형태의 소스 구조는 Console때 사용하던 구조와 같습니다. 즉, 프로그램이 Main 함수에서 실행하는 것입니다.

다시 말해서, 콘솔 프로젝트에서 System.Windows.Forms 라이브러리를 연결하고 위와 똑같은 소스를 작성하면 똑같이 윈도우 프로그램이 실행된다는 뜻입니다.

여기서 다시 생각나는 궁금증이 콘솔 창이 없는데? 라고 생각할 수 있는데, 탐색기에서 프로젝트를 오른쪽 마우스 클릭하면 Properties라는 메뉴가 있습니다.

클릭하면 설정 화면이 나오는데, 거기서 Output type이 Windows application으로 되어 있습니다.


일단 배포할 때는 Windows application으로 변경해야 하지만 우리는 일단 개발을 해야 하니 Console Application으로 바꿉시다.

다시 F5(Starting debugging)를 누르고 디버깅을 하면 Console 창이 나오는 것을 확인할 수 있습니다.

다시 돌아와서 Program.cs 파일에서는 new Form1()으로 인스턴스를 생성해서 Application.Run으로 실행하는 것을 확인할 수 있습니다.

이 Application.Run은 후반부에 다시 설명하겠습니다.


new Form1으로 인스턴스가 생성되었으니 Form1.cs를 확인해야 겠네요.

그런데 아마 Form1.cs파일을 클릭하면 디자인 창이 나올 것입니다.


그래서 오른쪽 마우스로 View Code로 소스를 봅시다.

아까 우리가 버튼을 클릭하면 실행하는 소스가 있네요. 확실히 구조는 Form1의 클래스이고 Form 클래스를 상속받았습니다.

생성자에서는 InitializeComponent함수가 실행되고 밑에는 뜬금없는 함수가 있어서 클릭하면 실행을 하네요. 이것만 보면 사실 프로그램이 이해가 안됩니다. 모든게 자동으로 처리되나? 프로그램은 자동이 없습니다. 다 이유가 있는 것입니다.


그럼 Form1.Desiner.cs 파일을 봅시다.

namespace WindowsFormsApp
{
  partial class Form1
  {
    /// <summary>
    /// Required designer variable.
    /// </summary>
    private System.ComponentModel.IContainer components = null;

    /// <summary>
    /// Clean up any resources being used.
    /// </summary>
    /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
    // 폼이 사라질 때 발생하는 함수(재정의 함수)
    protected override void Dispose(bool disposing)
    {
      if (disposing && (components != null))
      {
        components.Dispose();
      }
      base.Dispose(disposing);
    }

    #region Windows Form Designer generated code

    /// <summary>
    /// Required method for Designer support - do not modify
    /// the contents of this method with the code editor.
    /// </summary>
    // 생성자에서 호출된 함수
    private void InitializeComponent()
    {
      // 버튼 인스턴스 생성
      this.button1 = new System.Windows.Forms.Button();
      // 폼 레이아웃 설정
      this.SuspendLayout();
      // 
      // button1
      // 
      // 버튼 위치 설정
      this.button1.Location = new System.Drawing.Point(24, 26);
      // 버튼 이름
      this.button1.Name = "button1";
      // 버튼 크기
      this.button1.Size = new System.Drawing.Size(75, 23);
      // 버튼 탭 인덱스
      this.button1.TabIndex = 0;
      // 버튼 안의 Text
      this.button1.Text = "button1";
      // 버튼 색상 설정(기본 윈도우 색상)
      this.button1.UseVisualStyleBackColor = true;
      // 이벤트 추가
      this.button1.Click += new System.EventHandler(this.button1_Click);
      // 
      // Form1
      // 
      this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
      this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
      // 폼 크기 설정
      this.ClientSize = new System.Drawing.Size(253, 186);
      // 폼에 컨트럴 추가
      this.Controls.Add(this.button1);
      // 폼 이름
      this.Name = "Form1";
      // 폼 상단의 Text
      this.Text = "Form1";
      // 폼 레이아웃 설정
      this.ResumeLayout(false);
    }
    #endregion
    // button1 맴버 변수
    private System.Windows.Forms.Button button1;
  }
}

Form1 클래스가 partial로 분할이 되어 있습니다.

참조 - [C#] 54. namespace와 using 그리고 partial의 사용법

그리고 button1 클래스가 맴버 변수로 있고 InitializeComponent 함수 안에서 인스턴스 생성되네요.

그리고 여러가지 복잡한 설정이 있는데 무시하고 this.button1.Click 이란 함수를 보니, 이벤트를 추가하는 함수였네요.

즉, Form1.cs파일에 있는 함수는 button에서 클릭을 누르면 발생하는 이벤트 함수였던 것입니다.

참조 - [C#] 24. 이벤트(event) 키워드 사용법


그러면 Window의 속성을 변경하려면 Form.Designer.cs에서 수정을 해야 할까?

Visual studio는 고맙게도 디자인 화면에서 오른쪽 하단의 프로퍼티 설정을 보시면 속성을 설정할 수 있는 항목이 있습니다. 친절하게 하단에 속성 설명까지 나와 있습니다.


대충 Form의 구조는 알겠네요. 일단 Form1은 Form 클래스를 상속받았습니다.

즉, Form에서 기본적으로 윈도우에 폼을 그리고 여러가지 설정을 하는데, 기타 우리가 설정을 바꾸어야 하는 부분을 상속받아서 재정의를 하는 형식으로 프로그램이 구성되어 있네요.


그러면 Window란 어떤 방식으로 프로그램이 움직일까 설명하겠습니다.

Window는 기본적으로 스레드 환경에서 무한 루프 중입니다. 가장 비교하기 쉬운 예제가 플립북이 있습니다.

플립북이란 우리가 어렸을 때, 교과서에 첫장에 그림을 그리고 두번째 장에 조금 움직이는 그림을 그리고, 세번째 장에 더 움직이는 그림을 그리고해서 마지막에 책장을 넘기면 마치 그림이 움직이는 것처럼 보이는 것이 있습니다.

윈도우도 마찬가지고 스레드로 연속적으로 화면에 그리는 행위입니다. 그게 1초에 20장이 그려지면 20프레임(20FPS) 40장이 그려지면 40프레임(FPS)으로 표현하는 것입니다. 윈도우 폼은 우리가 FPS를 설정할 수는 없지만, 그런 식으로 그려지는 것입니다.


계속적으로 그려지는 것은 알겠는데, 그럼 버튼 클릭의 이벤트나 처리는 다른 스레드에서 실행되는 것인가? 아닙니다.

여러 스레드를 사용하게 되면 동기화 문제가 발생합니다. 각 스레드에서 어느 지점에서 서로 데이터를 주고 받아야 하는지에 대한 문제입니다.

링크 - [C#] 38. lock 키워드와 deadlock(교착 상태)


동기화 문제가 발생하기 때문에 기본적으로 단일 스레드로 무한 루프를 돌고 있습니다.

예로 button1_Click에서 MessageBox.Show()를 사용했는데 이번에는 for문을 0에서 1000까지 Thread.Sleep를 주고 실행해 보겠습니다.

using System;
using System.Windows.Forms;

namespace WindowsFormsApp
{
  public partial class Form1 : Form
  {
    // 폼 생성자
    public Form1()
    {
      // 초기화
      InitializeComponent();
    }
    // 버튼 클릭 이벤트
    private void button1_Click(object sender, EventArgs e)
    {
      // 반복문
      for(var i = 0; i < 1000; i++)
      {
        // 콘솔 출력
        Console.WriteLine(i);
        // 1초 대기
        System.Threading.Thread.Sleep(1000);
      }
    }
  }
}

프로그램을 실행해서 버튼을 눌러보면 뒤에 Console에서 i값이 출력되는 것을 확인할 수 있습니다. 그런데 윈도우를 조작하려면 멈춰 있습니다. 조금 시간이 지나면 응답 없음으로 바뀝니다.

즉, 단일 스레드라서 함수 등에서 위와 같이 인터럽트가 걸리면 프로그램이 멈춰버립니다. 그렇다면 윈도우 프로그램에서는 시간이 걸리는 작업이 안되는 것인가? 그건 Window 스레드를 이용하면 되는 데, 그건 다른 글에서 설명하겠습니다.


다시 돌아와서 윈도우는 단일 스레드의 무한 루프에서 돈다. 그럼 위와 같은 클릭이나 이벤트 등은 어떻게 처리가 되는 것인가 했을 때, 윈도우 메시지 큐가 있습니다. (큐 알고리즘인 FILO 구조입니다.)

출처 - https://www.researchgate.net

설명이 어려운데 그러니깐 위처럼 윈도우가 돌고 있습니다. 시스템에 윈도우 생성!하면 시스템에서 윈도우로 메시지를 보냅니다. 예를 들면 그려라, 무슨 함수를 실행해라 이렇게 말입니다.

그럼 우리 프로그램에서는 메시지를 받고 어느 위치에 이 윈도우는 있습니다. 하고 알려주면 시스템에서 모니터를 통해 윈도우를 그리는 것입니다.

using System;
using System.Windows.Forms;

namespace WindowsFormsApp
{
  public partial class Form1 : Form
  {
    // 폼 생성자
    public Form1()
    {
      // 초기화
      InitializeComponent();
    }
    // 버튼 클릭 이벤트
    private void button1_Click(object sender, EventArgs e) { }
    // 메시지 큐 함수의 재정의
    protected override void WndProc(ref Message m)
    {
      base.WndProc(ref m);
      // 콘솔 출력
      Console.WriteLine("0x{0:X4}", m.Msg);
    }
  }
}

WndProc라는 함수를 Form 클래스에서 재정의한 후 Console에 메시지를 출력하면 무슨 데이터가 끊임없이 출력되는 것을 볼 수 있습니다.

이게 메시지 번호인데 사실 C#에서는 메시지 큐를 직접적으로 컨트럴할 일은 없습니다.

그래서 이 식별자에 대한 값이 C#에는 정의되지 않았는데 C++(MFC)에는 정의되어 있습니다.

위 식별 코드로 시스템에서 윈도우 폼으로 끊임없이 메시지를 보내면 윈도우에서는 그 메시지를 받고 처리를 하는 것입니다.


사실 C#에서는 WndProc까지 재정의해서 사용할 일은 없습니다.

저도 실무로는 Window 프로그램 프로젝트 한번 해봤는데... 그 때는 제가 C#를 자세히 몰라서 WndProc에 덕지덕지 붙여서 개발했던 게 기억이 나네요. C#에서는 거의 모든 메시지가 이벤트 혹은 virtual로 재정의할 수 있게끔 클래스가 만들어져 있습니다.

개인적인 경험에 의해, C++(MFC)는 그런 기능이 없습니다. 그래서 이벤트나 특정 처리를 다 message에서 처리했던 게 기억이 납니다. Window message는 하나의 프로그램에서만 사용되는 게 아니라 시스템 전체에서 운영이 되는 것이기 때문에 간혹 message로 인스턴스를 넘기고 데이터를 넘기면, 받는 쪽에서 제대로 데이터를 받지를 못하고 객체를 찾아다녔던 게 기억이 많이 남네요.

어쩔 때는 다른 프로그램으로 데이터가 잘 못 날라가서 프로그램이 이상해 질 때도 있었습니다. 참고로 예전에 스타크래프트나 디아블로 게임 맵핵이 다 이런 Window message를 이용해서 프로그램 내의 값을 변조하는 것입니다. 특정 값이 변하면 맵이 켜진다거나..등등..

C++(MFC)에 비하면 C#은 정말 혁명적으로 윈도우 프로그램 개발이 쉬워진 것은 맞습니다. 아마 이런 부분 때문에 C++(MFC)이 C#보다 상대적으로 어렵다라고 표현하는 것 같네요.


여기까지 C#에서 윈도우 폼(Window form)을 작성하는 방법, 그리고 윈도우 메시지와 큐에 대한 글이었습니다.


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