2. Wafer 예광탄 [Wafer 코팅 시뮬레이션]

안녕하세요. 언제나 휴일입니다.

이번 강의는 Wafer 예광탄을 작성하는 실습입니다.

[그림] 실행 화면

이번 실습에서는 Wafer를 클래스로 정의할거예요.

그리고 Wafer를 코팅하는 모습을 시각화하는 부분을 Panel로 표현하는 작업을 진행합니다.

1. 프로젝트 생성 및 컨트롤 배치

C#, Windows, 데스크톱 형태의 Windows Forms 앱(.NET Framework) 프로젝트를 생성하세요.

여기에서는 “Wafer 예광탄” 이름으로 프로젝트를 생성했어요.(솔루션 이름은 Wafer 코팅 시뮬레이션)

[그림] 컨트롤 배치

그리고 Name 속성을 btn_start, pn_wafer, tm_coating으로 지정합니다.

Button, Panel, Timer를 추가합니다.

pn_wafer의 BorderStyle은 FixedSingle로 설정합니다.

2. Wafer 클래스 정의

Wafer는 클래스로 정의합시다. 프로젝트에 Wafer 클래스를 추가하세요.

이후에 라이브러리 WaferLineLib에 정의할 것이어서 접근지정자를 public으로 설정합니다.

    public class Wafer
    {
    }

Wafer는 1부터 순자적으로 일련번호를 부여하기로 합시다.

이를 위해 정적 멤버로 가장 최근에 부여한 Wafer 번호를 기억하는 멤버를 캡슐화합니다.

그리고 웨이퍼의 일련 번호는 읽기 전용으로 캡슐화합니다.

        static int last_wn;//가장 최근에 부여한 웨이퍼 일련 번호
        readonly int wn;//웨이퍼 일련 번호

하나의 Wafer에는 100개의 Cell이 있는 것으로 표현하기로 할게요.

그리고 현재 어느 Cell을 코팅하는지 기억하는 멤버도 캡슐화합니다.

        int[] cells = new int[100];
        int now;//현재 코팅할 쉘 번호

생성자에서는 웨이퍼 일련 번호를 순차적으로 부여합니다.

        /// <summary>
        /// 기본 생성자
        /// </summary>
        public Wafer()
        {
            last_wn++;
            wn = last_wn;//웨이퍼 번호 순차 부여
        }

현재 코팅하는 쉘 번호를 알 수 있게 속성(가져오기)를 제공합시다.

        /// <summary>
        /// 현재 코팅하고 있는 쉘 번호 속성 - 가져오기
        /// </summary>
        public int Now
        {
            get { return now; }
        }

코팅할 쉘을 다음 쉘로 증가하는 메서드를 제공합시다. 더 이상 이동할 수 없다면 실패를 반환합니다.

        /// <summary>
        /// 코팅할 쉘 번호 1증가 메서드
        /// </summary>
        /// <returns>다음 쉘이 유효하면 true, 유효하지 않으면 false</returns>
        public bool Increment()
        {
            if (now < 100)
            {
                now++;
                if (now == 100)//100번 인덱스 쉘은 없음 - 코팅 완료를 의미함
                {
                    return false;
                }
                return true;
            }
            return false;
        }

특정 품질로 현재 쉘을 코팅하는 메서드를 제공합시다.

        /// <summary>
        /// 코팅 메서드
        /// </summary>
        /// <param name="quality">코팅 품질</param>
        public void Coating(int quality)
        {
            if (Now < 100)
            {
                cells[Now] = quality;
            }
        }

Wafer의 쉘의 품질을 확인할 수 있는 인덱서를 제공합시다.

        /// <summary>
        /// 쉘의 품질에 접근하는 인덱서 - 가져오기
        /// </summary>
        /// <param name="index">쉘 인덱스(0~99)</param>
        /// <returns>쉘의 Quality</returns>
        public int this[int index]
        {
            get
            {
                if ((index < 0) || (index >= 100)) { return 0; }
                return cells[index];
            }
        }

Wafer의 평균 품질을 확인할 수 있게 속성(가져오기)을 제공합시다.

        /// <summary>
        /// 품질 속성(평균 품질) - 가져오기
        /// </summary>
        public double Quality
        {
            get
            {
                int sum = 0;
                foreach (int elem in cells)
                {
                    sum += elem;
                }
                return sum / 100.0;
            }
        }

Wafer의 번호와 평균 품질을 문자열로 반환하게 ToString 메서드를 재정의하세요.

        /// <summary>
        /// ToString 재정의
        /// </summary>
        /// <returns>웨이퍼 번호, 품질</returns>
        public override string ToString()
        {
            return string.Format("No:{0}, Quality:{1}", wn, Quality);
        }

다음은 Wafer.cs 전체 소스 코드입니다.

namespace Wafer_예광탄
{
    /// <summary>
    /// Wafer 클래스
    /// </summary>
    public class Wafer
    {
        static int last_wn;//가장 최근에 부여한 웨이퍼 일련 번호
        readonly int wn;//웨이퍼 일련 번호
        int[] cells = new int[100];
        int now;//현재 코팅할 쉘 번호
        /// <summary>
        /// 기본 생성자
        /// </summary>
        public Wafer()
        {
            last_wn++;
            wn = last_wn;//웨이퍼 번호 순차 부여
        }
        /// <summary>
        /// 현재 코팅하고 있는 쉘 번호 속성 - 가져오기
        /// </summary>
        public int Now
        {
            get { return now; }
        }
        /// <summary>
        /// 코팅할 쉘 번호 1증가 메서드
        /// </summary>
        /// <returns>다음 쉘이 유효하면 true, 유효하지 않으면 false</returns>
        public bool Increment()
        {
            if (now < 100)
            {
                now++;
                if (now == 100)//100번 인덱스 쉘은 없음 - 코팅 완료를 의미함
                {
                    return false;
                }
                return true;
            }
            return false;
        }
        /// <summary>
        /// 코팅 메서드
        /// </summary>
        /// <param name="quality">코팅 품질</param>
        public void Coating(int quality)
        {
            if (Now < 100)
            {
                cells[Now] = quality;
            }
        }
        /// <summary>
        /// 쉘의 품질에 접근하는 인덱서 - 가져오기
        /// </summary>
        /// <param name="index">쉘 인덱스(0~99)</param>
        /// <returns>쉘의 Quality</returns>
        public int this[int index]
        {
            get
            {
                if ((index < 0) || (index >= 100)) { return 0; }
                return cells[index];
            }
        }
        /// <summary>
        /// 품질 속성(평균 품질) - 가져오기
        /// </summary>
        public double Quality
        {
            get
            {
                int sum = 0;
                foreach (int elem in cells)
                {
                    sum += elem;
                }
                return sum / 100.0;
            }
        }
        /// <summary>
        /// ToString 재정의
        /// </summary>
        /// <returns>웨이퍼 번호, 품질</returns>
        public override string ToString()
        {
            return string.Format("No:{0}, Quality:{1}", wn, Quality);
        }
    }
}

3. Form1.cs 구현

Wafer 개체를 참조하는 속성을 캡슐화합시다.

    public partial class Form1 : Form
    {
        public Wafer Wafer
        {
            get;
            set;
        }
        ... 생략...

btn_start의 Click 이벤트 핸들러를 추가합니다.(이벤트 핸들러는 코드만 따라 치면 안 되는 것은 아시죠. 모르신다면 Windows Forms에 관한 학습을 먼저 하시기 바랍니다.)

이벤트 핸들러에서는 타이머를 활성화하고 btn_start를 비활성화합니다.

        private void btn_start_Click(object sender, System.EventArgs e)
        {
            tm_coating.Enabled = true;
            btn_start.Enabled = false;
        }

tm_coating 타이머의 Tick 이벤트 핸들러를 추가합니다.

현재 코팅 중인 Wafer 개체가 없으면 먼저 생성합니다.

70~100 사이의 품질 수준으로 Wafer를 코팅하는 코드를 작성합니다.

만약 코팅할 쉘 이동이 실패하면 모든 쉘을 코팅 완료한 것이므로 타이머를 비활성화하고 btn_start를 활성화합니다.

        Random rand = new Random(); 
        private void tm_coating_Tick(object sender, System.EventArgs e)
        {
            if(Wafer ==null)
            {
                Wafer = new Wafer();
            }
            Wafer.Coating(rand.Next(70, 100));
            if(Wafer.Increment()==false)
            {
                tm_coating.Enabled = false;
                btn_start.Enabled = true;
                Wafer = null;
            }
            pn_wafer.Invalidate();
        }
    }

pn_wafer의 Paint 이벤트 핸들러를 추가합니다.

이에 관한 부분은 동영상 강의를 참고하세요.

        private void pn_wafer_Paint(object sender, PaintEventArgs e)
        {
            Graphics graphics = e.Graphics;
            Brush brush = Brushes.Silver;
            Rectangle rect = new Rectangle(0, 0, pn_wafer.Width, pn_wafer.Height);
            graphics.FillEllipse(brush, rect);
            int width = pn_wafer.Width;
            int height = pn_wafer.Height;
            int sx = (int)(width * 0.15);
            int sy = (int)(height * 0.15);
            Rectangle rect2 = new Rectangle(sx, sy, (int)(width * 0.7), (int)(height * 0.7));
            graphics.DrawRectangle(Pens.Red, rect2);
            int xu = rect2.Width / 10;
            int yu = rect2.Height / 10;
            Pen pen = new Pen(Color.DarkGray, 1);
            for (int x = 1; x < 10; x++)
            {
                graphics.DrawLine(pen, new Point(sx + x * xu, sy), new Point(sx + x * xu, sy + rect2.Height));
            }
            for (int y = 1; y < 10; y++)
            {
                graphics.DrawLine(pen, new Point(sx, sy + y * yu), new Point(sx + rect2.Width, sy + y * yu));
            }

            if (Wafer == null)
            {
                return;
            }
            int dir = 0;
            int step = 1, nowstep = step;
            int nx = 4, ny = 5;
            for (int i = 0; i < 100; i++)
            {
                int qual = Wafer[i];
                Color color = Color.FromArgb(200 - qual * 2, 55 + qual * 2, 0);
                Rectangle rt = new Rectangle(sx + nx * xu + 1, sy + ny * yu + 1, xu - 1, yu - 1);
                graphics.FillRectangle(new SolidBrush(color), rt);

                if (nowstep == 0)
                {
                    dir = (dir + 1) % 4;
                    if (dir % 2 == 0)
                    {
                        step++;
                    }
                    nowstep = step;
                }
                nowstep--;
                switch (dir)
                {
                    case 0: ny--; break;
                    case 1: nx++; break;
                    case 2: ny++; ; break;
                    case 3: nx--; break;
                }
            }
        }

전체 코드는 다음과 같습니다.

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

namespace Wafer_예광탄
{
    public partial class Form1 : Form
    {
        public Wafer Wafer
        {
            get;
            set;
        }
        public Form1()
        {
            InitializeComponent();            
        }

        private void pn_wafer_Paint(object sender, PaintEventArgs e)
        {
            Graphics graphics = e.Graphics;
            Brush brush = Brushes.Silver;
            Rectangle rect = new Rectangle(0, 0, pn_wafer.Width, pn_wafer.Height);
            graphics.FillEllipse(brush, rect);
            int width = pn_wafer.Width;
            int height = pn_wafer.Height;
            int sx = (int)(width * 0.15);
            int sy = (int)(height * 0.15);
            Rectangle rect2 = new Rectangle(sx, sy, (int)(width * 0.7), (int)(height * 0.7));
            graphics.DrawRectangle(Pens.Red, rect2);
            int xu = rect2.Width / 10;
            int yu = rect2.Height / 10;
            Pen pen = new Pen(Color.DarkGray, 1);
            for (int x = 1; x < 10; x++)
            {
                graphics.DrawLine(pen, new Point(sx + x * xu, sy), new Point(sx + x * xu, sy + rect2.Height));
            }
            for (int y = 1; y < 10; y++)
            {
                graphics.DrawLine(pen, new Point(sx, sy + y * yu), new Point(sx + rect2.Width, sy + y * yu));
            }

            if (Wafer == null)
            {
                return;
            }
            int dir = 0;
            int step = 1, nowstep = step;
            int nx = 4, ny = 5;
            for (int i = 0; i < 100; i++)
            {
                int qual = Wafer[i];
                Color color = Color.FromArgb(200 - qual * 2, 55 + qual * 2, 0);
                Rectangle rt = new Rectangle(sx + nx * xu + 1, sy + ny * yu + 1, xu - 1, yu - 1);
                graphics.FillRectangle(new SolidBrush(color), rt);

                if (nowstep == 0)
                {
                    dir = (dir + 1) % 4;
                    if (dir % 2 == 0)
                    {
                        step++;
                    }
                    nowstep = step;
                }
                nowstep--;
                switch (dir)
                {
                    case 0: ny--; break;
                    case 1: nx++; break;
                    case 2: ny++; ; break;
                    case 3: nx--; break;
                }
            }
        }

        private void btn_start_Click(object sender, System.EventArgs e)
        {
            tm_coating.Enabled = true;
            btn_start.Enabled = false;
        }

        Random rand = new Random(); 
        private void tm_coating_Tick(object sender, System.EventArgs e)
        {
            if(Wafer ==null)
            {
                Wafer = new Wafer();
            }
            Wafer.Coating(rand.Next(70, 100));
            if(Wafer.Increment()==false)
            {
                tm_coating.Enabled = false;
                btn_start.Enabled = true;
                Wafer = null;
            }
            pn_wafer.Invalidate();
        }
    }
}

4. 더블 버퍼링 가능한 패널 클래스 정의하기

현재까지 작성한 것으로 실행해서 버튼을 클릭하면 패널에 코팅하는 부분이 깜빡임이 심해 눈이 피곤합니다.

이러한 현상은 Windows 앱에서 Paint 이벤트는 우선 순위가 낮기 때문입니다.

이를 해결하기 위한 방법 중에 하나가 더블 버퍼링입니다.

DPanel 클래스를 추가하여 다음처럼 편집합니다.

using System.Windows.Forms;

namespace Wafer_예광탄
{
    /// <summary>
    /// DPanel 클래스 - 더블버퍼링 패널
    /// </summary>
    public class DPanel : Panel
    {
        /// <summary>
        /// 기본 생성자
        /// </summary>
        public DPanel()
        {
            SetStyle(System.Windows.Forms.ControlStyles.OptimizedDoubleBuffer | System.Windows.Forms.ControlStyles.UserPaint | System.Windows.Forms.ControlStyles.AllPaintingInWmPaint, true);
            UpdateStyles();
        }
    }
}

5. Form1.Designer.cs 수정

그리고 Form1.Designer.cs에서 pn_wafer를 생성하는 코드를 DPanel 개체를 생성하는 것으로 수정합니다.

        private void InitializeComponent()
        {
            this.components = new System.ComponentModel.Container();
            this.tm_coating = new System.Windows.Forms.Timer(this.components);
            this.btn_start = new System.Windows.Forms.Button();
            //this.pn_wafer = new System.Windows.Forms.Panel();
            this.pn_wafer = new Wafer_예광탄.DPanel();
            ... 중략...

다음은 Form1.Designer.cs의 전체 소스 코드입니다.

namespace Wafer_예광탄
{
    partial class Form1
    {
        /// <summary>
        /// 필수 디자이너 변수입니다.
        /// </summary>
        private System.ComponentModel.IContainer components = null;

        /// <summary>
        /// 사용 중인 모든 리소스를 정리합니다.
        /// </summary>
        /// <param name="disposing">관리되는 리소스를 삭제해야 하면 true이고, 그렇지 않으면 false입니다.</param>
        protected override void Dispose(bool disposing)
        {
            if (disposing && (components != null))
            {
                components.Dispose();
            }
            base.Dispose(disposing);
        }

        #region Windows Form 디자이너에서 생성한 코드

        /// <summary>
        /// 디자이너 지원에 필요한 메서드입니다. 
        /// 이 메서드의 내용을 코드 편집기로 수정하지 마세요.
        /// </summary>
        private void InitializeComponent()
        {
            this.components = new System.ComponentModel.Container();
            this.tm_coating = new System.Windows.Forms.Timer(this.components);
            this.btn_start = new System.Windows.Forms.Button();
            //this.pn_wafer = new System.Windows.Forms.Panel();
            this.pn_wafer = new Wafer_예광탄.DPanel();
            this.SuspendLayout();
            // 
            // tm_coating
            // 
            this.tm_coating.Tick += new System.EventHandler(this.tm_coating_Tick);
            // 
            // btn_start
            // 
            this.btn_start.Location = new System.Drawing.Point(105, 23);
            this.btn_start.Name = "btn_start";
            this.btn_start.Size = new System.Drawing.Size(75, 23);
            this.btn_start.TabIndex = 1;
            this.btn_start.Text = "시작";
            this.btn_start.UseVisualStyleBackColor = true;
            this.btn_start.Click += new System.EventHandler(this.btn_start_Click);
            // 
            // pn_wafer
            // 
            this.pn_wafer.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
            this.pn_wafer.Location = new System.Drawing.Point(65, 66);
            this.pn_wafer.Name = "pn_wafer";
            this.pn_wafer.Size = new System.Drawing.Size(144, 144);
            this.pn_wafer.TabIndex = 0;
            this.pn_wafer.Paint += new System.Windows.Forms.PaintEventHandler(this.pn_wafer_Paint);
            // 
            // Form1
            // 
            this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 12F);
            this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
            this.ClientSize = new System.Drawing.Size(293, 271);
            this.Controls.Add(this.btn_start);
            this.Controls.Add(this.pn_wafer);
            this.Name = "Form1";
            this.Text = "Wafer Panel 예광탄";
            this.ResumeLayout(false);

        }

        #endregion
        private System.Windows.Forms.Timer tm_coating;
        private System.Windows.Forms.Button btn_start;
        private DPanel pn_wafer;
    }
}