[C#] 58. 원도우 폼(Window form)에서 컨트럴(Control) 다루기


Study/C#  2021. 10. 29. 13:33

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


이 글은 C#의 원도우 폼(Window form)에서 컨트럴(Control) 다루기에 대한 글입니다.


이전 글에서 Window form을 만드는 방법에 대해서 간략하게 설명했습니다.

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


일반적으로 우리가 윈도우 프로그램을 만들게 되면 가장 많이 사용하는 클래스 객체는 아마도 컨트럴(Control) 객체입니다.

이 컨트럴 객체는 기본적으로 .Net framework에서 제공을 하고 있습니다.

사실 이 기본적인 컨트럴(Control)만 사용해도 거의 모든 윈도우 프로그램을 만들 수 있습니다. 사양에 따라 필요한 컨트럴(Control)를 개발할 일도 있겠지만, 제 생각에는 많은 프로젝트가 기본적으로 제공하는 컨트럴만으로도 충분히 개발이 가능하다고 생각됩니다.


먼저 폼에 컨트럴을 추가하는 방법은 디자인 화면에서 드래그 엔 드롭(Drag and drop)을 이용해서 추가할 수 있고 또는 Form1.Designer.cs 페이지에서 소스를 작성해서 추가할 수 있습니다.

여러가지 간단하게 컨트럴을 추가하는 부분은 역시 디자인에서 작업을 하는게 좋지만, 역시 디자인에서 추가를 하게 되면 세밀한 설정은 힘든 부분이 있습니다.

그럼 Form1.Designer.cs에서 추가하는 방법입니다.

// Designer 코드는 왜인지 namespace 선언이 되어 있지 않습니다.
// 자주 사용하는 라이브러리의 namespace를 선언을 하는 것이 좋습니다.
using System.Windows.Forms;

namespace WindowsFormsApp
{
  // 초기에 생성되면 이런 저런 주석등이 작성되어 있는 데 깔끔하게 지우고 시작하는 것이 좋습니다.
  partial class Form1
  {
    // 리소스 관리 변수(이건 건드릴 일 없으니 그대로 냅두자)
    private System.ComponentModel.IContainer components = null;
    // button 컨트럴 맴버 변수
    private Button button = null;
    // 초기화 함수
    private void InitializeComponent()
    {
      // 인스턴스 추가(위에 using을 사용했는데도, Visual studio에서 자동으로 namespace가 붙네요.)
      this.button = new System.Windows.Forms.Button();
      // 레이 아웃 설정
      this.SuspendLayout();
      // 
      // button
      // 
      // 버튼 위치 설정(GDI 좌표계, 윈도우 왼쪽 위 상단이 0,0입니다. 한 픽셀마다 1씩 이동한다.)
      this.button.Location = new System.Drawing.Point(12, 12);
      // 컨트럴 이름 설정
      this.button.Name = "TestBtn";
      // 버튼 안의 텍스트
      this.button.Text = "TestBtn";
      // 컨트럴 크기 설정
      this.button.Size = new System.Drawing.Size(75, 23);
      // TabIndex 설정
      this.button.TabIndex = 0;
      // 
      // 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.button);
      // 폼 이름
      this.Name = "Form1";
      // 폼 상단의 Text
      this.Text = "TestButton";
      // 폼 레이아웃 설정
      this.ResumeLayout(false);
    }
    // 초기에는 이게 클래스의 위쪽에 작성되어 있는데, 그렇게 중요한 함수가 아니기 때문에 아래쪽으로 옮깁니다.
    protected override void Dispose(bool disposing)
    {
      // 프로그램이 닫힐 때, 작동되는 리소스 해제.
      if (disposing && (components != null))
      {
        components.Dispose();
      }
      base.Dispose(disposing);
    }
  }
}

제가 Window 폼에서 컨트럴을 작성하는 방법은 일단 Designer 소스 화면에서 사용할 컨트럴 맴버 변수와 인스턴스 생성, 그리고 Name 설정(이게 가장 중요함), Text 설정, 그리고 Controls.Add를 통해 Window 폼에 컨트럴 설정을 추가합니다. 다음에 다시 디자인 모드 화면으로 와서 컨트럴을 윈도우 폼에 알맞게 배치, 크기 설정을 합니다.

그리고 다시 소스로 돌아와서 정밀한(?) 설정을 합니다.

왜 이렇게 복잡하게 작성할까 싶겠지만, 솔직히 Visual Studio에서 제공하는 디자인 모드는 굉장히 편하기는 합니다. 그런데 너무 자동으로 설정하는 부분이 많아서 놓치는 부분이 많아 집니다.

예를 들면 Name 항목입니다. 이 Name 항목이 Window Form에서 사소한 것 같아도 컨트럴을 식별하기 위해 굉장히 중요한 부분입니다.


우리가 맴버 변수를 선언해서 맴버 변수에서 직접 컨트럴 인스턴스를 취득하면 문제가 없을 것 같지만, 그런 Form 클래스 안에서만 입니다. 다른 컨트럴에서는 어떨까요? Form의 인스턴스를 넘겨서 맴버 변수를 전부 프로퍼티로 해서 취득해 오지 않는 이상 어떤 컨트럴이 어떤 컨트럴인지 알 수가 없습니다.

이것에 관해서는 밑에서 자세히 설명하겠습니다.


그렇기 때문에 디자인 모드에서 작성하는 것도 중요하지만 Form.Designer.cs 소스 파일에서도 제대로 설정이 되어있나, 임의의 변수명이 아닌 조금은 구별하기 쉬운 변수명으로 작성이 되었나 정도를 확인하기 위해서 소스에서도 설정하는 것이 중요합니다.

여기까지 Form에서 Control를 추가하는 것을 설명했습니다.


그럼 이제 Control를 자세히 설명하겠습니다.

Window에서 제공하는 모든 컨트럴은 Control 클래스를 상속 받았습니다. 마치 클래스가 Object 클래스를 상속받는 것처럼 말입니다.

Button만 봐도 ButtonBase를 상속받고 ButtonBase는 Control를 상속받았습니다.

즉, 우리가 이 Control를 상속받으면 컨트럴을 만들 수가 있다는 뜻입니다.


그럼 클래스를 추가하고 컨트럴을 만들어 봅시다.

using System;
using System.Windows.Forms;

namespace WindowsFormsApp
{
  // TestControl 생성
  class TestControl : Control
  {
    // 프로퍼티 재정의
    public override string Text
    {
      // 취득은 가능하지만
      get => base.Text;
      // 설정은 못함.
      set { }
    }
    // 생성자
    public TestControl()
    {
      // 초기 Text 설정
      base.Text = "Hello World";
      // 초기 사이즈 설정
      base.Size = new Size(100, 22);
    }
    // 그리기 이벤트
    protected override void OnPaint(PaintEventArgs e)
    {
      base.OnPaint(e);
      // 폼 취득
      var form = FindForm();
      // Text 그리기
      e.Graphics.DrawString(base.Text, form.Font, Brushes.Red, new PointF(0, 0));
    }
  }
}

사실 OnPaint를 알려먼 GDI에 대해서 알아야 하는데, GDI는 다른 글에서 자세히 설명하고 여기서는 간단하게 위처럼 작성합시다.

TestControl은 인스턴스 외부에서는 설정할 수 없습니다. 설정한다고 해도 아무런 처리가 일어나지 않게 되죠. 그럼 어디서 설정할까 생성자에서 Hello world로 설정했습니다.

그리고 다시 Visual studio의 디자이인 모드 화면으로 돌아가 보겠습니다.

그럼 Toolbox에 TestControl이 추가된 것을 확인 할 수 있습니다. (만약 없으면 F5를 누르고 디버깅하면 생깁니다.)

그럼 다시 Button처럼 드래그 앤 드롭(Drag and drop)으로 컨트럴을 추가합니다. 그리고 Form.Designer.cs에서도 변수명과 Name등의 설정을 합시다.

using System.Windows.Forms;

namespace WindowsFormsApp
{
  partial class Form1
  {
    // 리소스 관리 변수(이건 건드릴 일 없으니 그대로 냅두자)
    private System.ComponentModel.IContainer components = null;
    // button 컨트럴 맴버 변수
    private Button button = null;
    // 새로 생성한 컨트럴 맴버 변수
    private TestControl testCtl = null;
    // 초기화 함수
    private void InitializeComponent()
    {
      // 인스턴스 생성
      this.button = new System.Windows.Forms.Button();
      this.testCtl = new WindowsFormsApp.TestControl();
      // 레이 아웃 설정
      this.SuspendLayout();
      // 
      // button
      // 
      // 버튼 위치 설정(GDI 좌표계, 윈도우 왼쪽 위 상단이 0,0입니다. 한 픽셀마다 1씩 이동한다.)
      this.button.Location = new System.Drawing.Point(27, 40);
      // 버튼 안의 텍스트
      this.button.Name = "TestBtn";
      // 컨트럴 크기 설정
      this.button.Size = new System.Drawing.Size(75, 23);
      // TabIndex 설정
      this.button.TabIndex = 0;
      // 컨트럴에 사용될 Text 이름 설정
      this.button.Text = "TestBtn";
      // 
      // testCtl
      // 
      // 위치 설정
      this.testCtl.Location = new System.Drawing.Point(27, 12);
      // 컨트럴 이름 설정
      this.testCtl.Name = "testCtl";
      // 크기 설정
      this.testCtl.Size = new System.Drawing.Size(100, 22);
      // TabIndex 설정
      this.testCtl.TabIndex = 1;
      // Text 설정(의미가 없다)
      this.testCtl.Text = "Hello World";
      // 
      // 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.button);
      this.Controls.Add(this.testCtl);
      // 폼 이름 설정
      this.Name = "Form1";
      // 폼 상단의 Text
      this.Text = "TestButton";
      // 폼 레이아웃 설정
      this.ResumeLayout(false);
    }

    // 초기에는 이게 클래스의 위쪽에 작성되어 있는데, 그렇게 중요한 함수가 아니기 때문에 아래쪽으로 옮깁니다.
    protected override void Dispose(bool disposing)
    {
      // 프로그램이 닫힐 때, 작동되는 리소스 해제.
      if (disposing && (components != null))
      {
        components.Dispose();
      }
      base.Dispose(disposing);
    }
  }
}

위처럼 설정하고 실행하면 윈도우 폼에서 라벨 같은 컨트럴이 생기는 것을 확인할 수 있습니다.

이제부터 제가 만든 컨트럴에서 button 이벤트를 받아오겠습니다.

button 이벤트에서 클릭하면 텍스트 내용이 Hello world에서 Click!으로 바뀌는 내용입니다.

using System;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;

namespace WindowsFormsApp
{
  // TestControl 생성
  class TestControl : Control
  {
    // 프로퍼티 재정의
    public override string Text
    {
      // 취득은 가능하지만
      get => base.Text;
      // 설정은 못함.
      set { }
    }
    // 생성자
    public TestControl()
    {
      // 초기 Text 설정
      base.Text = "Hello World";
      // 초기 사이즈 설정
      base.Size = new Size(100, 22);
    }
    // 생성자에 넣으면 Form에 컨트럴이 추가되기 전이기 때문에 초기 설정 이외에는 OnCreateControl 이벤트에 넣는다.
    protected override void OnCreateControl()
    {
      base.OnCreateControl();
      // 폼 취득
      var form = FindForm();
      // 폼에서 TestBtn의 이름을 가진 버튼을 취득한다.
      var button = (from Control c in form.Controls where c is Button && "TestBtn".Equals(c.Name) select c).FirstOrDefault();
      // null 이 아니면
      if (button != null)
      {
        // 이벤트 추가
        button.Click += Button_Click;
      }
    }
    // 버튼 클릭 이벤트
    private void Button_Click(object sender, EventArgs e)
    {
      // 텍스트를 바꿉니다.
      base.Text = "Click!";
      // OnPaint 호출
      Invalidate();
    }
    // 그리기
    protected override void OnPaint(PaintEventArgs e)
    {
      base.OnPaint(e);
      // 폼 취득
      var form = FindForm();
      // Text 그리기
      e.Graphics.DrawString(base.Text, form.Font, Brushes.Red, new PointF(0, 0));
    }
  }
}

실행을 해서 버튼을 클릭하면 TestControl의 Text가 Hello world부터 Click!으로 바뀌는 것을 확인할 수 있습니다.

위 소스에서 보면 Control에서 Form의 인스턴스를 취득하는 함수는 FindForm()입니다. 물론 생성자에서 컨트럴을 생성할 때, Form의 인스턴스를 파라미터로 넘기는 것도 가능하지만, Window form를 만들때 소스 코딩 규약을 지키지 않으면 Visual studio에서 디자인 모드가 에러가 발생할 수 있습니다.

그래서 일반적으로는 Control에서는 Form의 인스턴스를 FindForm()함수를 이용해서 취득합니다. 그렇다면 Form의 인스턴스에서 버튼 인스턴스를 어떻게 찾을까?


그렇습니다. 위에서 설정한 Control의 Name으로 찾습니다. 물론 버튼의 인스턴스를 프로퍼티로 public을 설정하면 Name으로 찾을 필요는 없지만, 실제로 윈도우 프로그램을 개발하다보면 많은 컨트럴을 사용하게 됩니다.

그럼 그 모든 컨트럴을 프로퍼티로 생성하게 되면 꽤 소스가 지저분해 지겠네요. 또, FindForm으로 취득해 오는 타입은 Form 클래스 타입입니다. 그렇다면 Form1로 강제 캐스팅을 해서 형 변환도 해야 할 것입니다.

위 예제에서는 Dialog 형식으로 하나의 폼(SMI:Single Document Interface)이지만 MDI(Multiple Document Interface)이라면 많은 Form을 사용하게 됩니다.

다중 Form 환경에서 TestControl를 사용한다고 생각하면 강제 캐스팅을 하게 되면 충분히 에러가 발생할 여지가 있습니다.


그렇기 때문에 컨트럴에서 폼의 인스턴스를 취득하는 방법은 FindForm()함수를 통해서, 다른 컨트럴을 찾는 방법에 대해서는 Name을 이용해서 찾는 방법이 안전합니다.

그래서 컨트럴에서 Name을 설정하는 것은 컨트럴 식별을 위해 꽤 중요한 작업입니다.


여기까지 C#의 원도우 폼(Window form)에서 컨트럴(Control) 다루기에 대한 글이었습니다.


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